4 Commits

29 changed files with 1331 additions and 1222 deletions

View File

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

810
app.py
View File

@@ -22,6 +22,16 @@ from functions.database import (
init_database,
EstadoMilitante,
Endereco,
TipoComprovante,
Comprovante,
CampanhaFinanceira,
TransacaoPIX,
Permission,
Role,
Atividade,
MaterialAtividade,
Relatorio,
CentralizacaoComprovante
)
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, joinedload
@@ -34,7 +44,7 @@ from time import time
from flask_mail import Mail, Message
import os
from dotenv import load_dotenv
from functions.rbac import Role, Permission, init_rbac
from functions.rbac import init_rbac
from functions.decorators import require_permission, require_role, require_minimum_role, require_login, require_instance_permission, require_instance_access
import re
import secrets
@@ -63,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)
@@ -78,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"""
@@ -204,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"))
# 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 user.otp_secret and not user.verify_otp(otp):
flash("Código OTP inválido.", "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()
@@ -274,25 +288,20 @@ def create_app():
# Buscar últimos pagamentos
ultimos_pagamentos = db.query(Pagamento)\
.join(Militante)\
.order_by(Pagamento.data_pagamento.desc())\
.limit(5)\
.all()
# Buscar tipos de pagamento
tipos_pagamento = db.query(TipoPagamento).all()
return 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_pagamentos=ultimos_pagamentos,
tipos_pagamento=tipos_pagamento,
Militante=Militante)
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
@@ -713,190 +722,240 @@ def create_app():
# Rota para criar um novo material vendido
@app.route("/materiais/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.VIEW_CELL_REPORTS)
@require_permission(Permission.MANAGE_MATERIALS)
def novo_material():
db = get_db_connection()
try:
militante_id = request.form.get('militante_id')
tipo_material_id = request.form.get('tipo_material_id')
descricao = request.form.get('descricao')
valor = float(request.form.get('valor'))
data_venda = converter_data(request.form.get('data_venda'))
if request.method == "POST":
try:
nome = request.form.get("nome")
descricao = request.form.get("descricao")
preco = float(request.form.get("preco"))
quantidade = int(request.form.get("quantidade"))
material = MaterialVendido(
militante_id=militante_id,
tipo_material_id=tipo_material_id,
descricao=descricao,
valor=valor,
data_venda=data_venda
)
if not all([nome, descricao, preco, quantidade]):
flash("Todos os campos são obrigatórios", "danger")
return redirect(url_for("novo_material"))
db.add(material)
db.commit()
db = get_db_connection()
material = MaterialVendido(nome=nome, valor=valor)
db.add(material)
db.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Material cadastrado com sucesso!'
})
flash("Material cadastrado com sucesso", "success")
return redirect(url_for("listar_materiais"))
except Exception as e:
flash(f"Erro ao cadastrar material: {str(e)}", "danger")
return redirect(url_for("novo_material"))
return render_template("novo_material.html")
flash('Material cadastrado com sucesso!', 'success')
return redirect(url_for('listar_materiais'))
except Exception as e:
db.rollback()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao cadastrar material. Por favor, tente novamente.'
}), 400
flash('Erro ao cadastrar material. Por favor, tente novamente.', 'error')
return redirect(url_for('listar_materiais'))
finally:
db.close()
# Rota para listar materiais vendidos
@app.route("/materiais")
@require_login
@require_permission(Permission.VIEW_CELL_REPORTS)
@require_permission(Permission.MANAGE_MATERIALS)
def listar_materiais():
db = get_db_connection()
try:
materiais = db.query(MaterialVendido).join(Militante).join(TipoMaterial).all()
militantes = db.query(Militante).all()
tipos_material = db.query(TipoMaterial).all()
return render_template('listar_materiais.html',
materiais=materiais,
militantes=militantes,
tipos_material=tipos_material)
finally:
db.close()
materiais = db.query(MaterialVendido).all()
return render_template("listar_materiais.html", materiais=materiais)
@app.route("/materiais/vendidos/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.MANAGE_MATERIALS)
def novo_material_vendido():
if request.method == "POST":
try:
militante_id = request.form.get("militante_id")
material_id = request.form.get("material_id")
quantidade = request.form.get("quantidade")
data_venda = request.form.get("data_venda")
if not all([militante_id, material_id, quantidade, data_venda]):
flash("Todos os campos são obrigatórios", "danger")
return redirect(url_for("novo_material_vendido"))
db = get_db_connection()
material_vendido = MaterialVendido(
militante_id=militante_id,
material_id=material_id,
quantidade=quantidade,
data_venda=data_venda
)
db.add(material_vendido)
db.commit()
flash("Material vendido registrado com sucesso", "success")
return redirect(url_for("listar_materiais_vendidos"))
except Exception as e:
flash(f"Erro ao registrar material vendido: {str(e)}", "danger")
return redirect(url_for("novo_material_vendido"))
db = get_db_connection()
militantes = db.query(Militante).all()
materiais = db.query(MaterialVendido).all()
return render_template("novo_material_vendido.html",
militantes=militantes,
materiais=materiais)
@app.route("/materiais/vendidos")
@require_login
@require_permission(Permission.MANAGE_MATERIALS)
def listar_materiais_vendidos():
db = get_db_connection()
materiais_vendidos = db.query(MaterialVendido).all()
return render_template("listar_materiais_vendidos.html",
materiais_vendidos=materiais_vendidos)
# Rota para criar uma nova venda de jornal
@app.route("/jornais/novo", methods=["GET", "POST"])
@app.route("/vendas/jornal/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.VIEW_CELL_REPORTS)
@require_permission(Permission.MANAGE_MATERIALS)
def nova_venda_jornal():
if request.method == "POST":
try:
militante_id = request.form.get('militante_id')
quantidade = int(request.form.get('quantidade'))
valor_total = float(request.form.get('valor_total'))
data_venda = converter_data(request.form.get('data_venda'))
militante_id = request.form.get("militante_id")
quantidade = request.form.get("quantidade")
data_venda = request.form.get("data_venda")
if not validar_data(data_venda):
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Data de venda inválida ou futura'
}), 400
flash('Data de venda inválida ou futura', 'danger')
return redirect(url_for('nova_venda_jornal'))
if not all([militante_id, quantidade, data_venda]):
flash("Todos os campos são obrigatórios", "danger")
return redirect(url_for("nova_venda_jornal"))
db = get_db_connection()
venda = VendaJornalAvulso(
venda_jornal = VendaJornalAvulso(
militante_id=militante_id,
quantidade=quantidade,
valor_total=valor_total,
data_venda=data_venda
)
db.add(venda)
db.add(venda_jornal)
db.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'success',
'message': 'Venda cadastrada com sucesso!'
})
flash('Venda cadastrada com sucesso!', 'success')
return redirect(url_for('listar_vendas_jornal'))
flash("Venda de jornal registrada com sucesso", "success")
return redirect(url_for("listar_vendas_jornal"))
except Exception as e:
db.rollback()
app.logger.error(f"Erro ao cadastrar venda: {e}")
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({
'status': 'error',
'message': 'Erro ao cadastrar venda'
}), 400
flash('Erro ao cadastrar venda', 'danger')
return redirect(url_for('nova_venda_jornal'))
finally:
db.close()
flash(f"Erro ao registrar venda de jornal: {str(e)}", "danger")
return redirect(url_for("nova_venda_jornal"))
# Rota para listar vendas de jornal
@app.route("/jornais")
db = get_db_connection()
militantes = db.query(Militante).all()
return render_template("nova_venda_jornal.html", militantes=militantes)
@app.route("/vendas/jornal")
@require_login
@require_permission(Permission.VIEW_CELL_REPORTS)
@require_permission(Permission.MANAGE_MATERIALS)
def listar_vendas_jornal():
db = get_db_connection()
try:
vendas = db.query(VendaJornalAvulso).join(Militante).all()
militantes = db.query(Militante).all()
return render_template('listar_vendas_jornal.html',
vendas=vendas,
militantes=militantes)
finally:
db.close()
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
@app.route("/relatorios/cotas/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.VIEW_CELL_REPORTS)
@require_permission(Permission.MANAGE_REPORTS)
def novo_relatorio_cotas():
if request.method == "POST":
try:
setor_id = request.form.get("setor_id")
comite_id = request.form.get("comite_id")
total_cotas = float(request.form.get("total_cotas"))
data_relatorio = request.form.get("data_relatorio")
data_inicio = request.form.get("data_inicio")
data_fim = request.form.get("data_fim")
# Validar data
if not validar_data(data_relatorio):
flash('Data do relatório inválida', 'danger')
return render_template("novo_relatorio_cotas.html")
# Converter data
data_relatorio = converter_data(data_relatorio)
# Validar data futura
if data_relatorio > date.today():
flash('A data do relatório não pode ser futura', 'danger')
return render_template("novo_relatorio_cotas.html")
if not all([data_inicio, data_fim]):
flash("Todos os campos são obrigatórios", "danger")
return redirect(url_for("novo_relatorio_cotas"))
db = get_db_connection()
try:
relatorio_cotas_mensais = RelatorioCotasMensais(
setor_id=setor_id,
comite_id=comite_id,
total_cotas=total_cotas,
data_relatorio=data_relatorio
)
db.add(relatorio_cotas_mensais)
db.commit()
flash('Relatório de cotas cadastrado com sucesso!', 'success')
return redirect(url_for('listar_relatorios_cotas'))
except Exception as e:
db.rollback()
app.logger.error(f"Erro ao cadastrar relatório de cotas: {e}")
flash('Erro ao cadastrar relatório de cotas', 'danger')
return render_template("novo_relatorio_cotas.html")
finally:
db.close()
except ValueError as e:
flash(str(e), 'danger')
return render_template("novo_relatorio_cotas.html")
cotas = db.query(CotaMensal).filter(
CotaMensal.data_cota.between(data_inicio, data_fim)
).all()
db = get_db_connection()
try:
setores = db.query(Setor).order_by(Setor.nome).all()
comites = db.query(ComiteCentral).order_by(ComiteCentral.nome).all()
return render_template("novo_relatorio_cotas.html",
setores=setores,
comites=comites,
hoje=date.today().strftime('%Y-%m-%d'))
finally:
db.close()
return render_template("relatorio_cotas.html",
cotas=cotas,
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_cotas"))
return render_template("novo_relatorio_cotas.html")
@app.route("/relatorios/jornais/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.MANAGE_REPORTS)
def novo_relatorio_jornais():
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_jornais"))
db = get_db_connection()
jornais = db.query(VendaJornalAvulso).filter(
VendaJornalAvulso.data_venda.between(data_inicio, data_fim)
).all()
return render_template("relatorio_jornais.html",
jornais=jornais,
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_jornais"))
return render_template("novo_relatorio_jornais.html")
@app.route("/relatorios/assinaturas/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.MANAGE_REPORTS)
def novo_relatorio_assinaturas():
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_assinaturas"))
db = get_db_connection()
assinaturas = db.query(AssinaturaAnual).filter(
AssinaturaAnual.data_assinatura.between(data_inicio, data_fim)
).all()
return render_template("relatorio_assinaturas.html",
assinaturas=assinaturas,
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_assinaturas"))
return render_template("novo_relatorio_assinaturas.html")
@app.route("/relatorios/campanhas/novo", methods=["GET", "POST"])
@require_login
@require_permission(Permission.MANAGE_REPORTS)
def novo_relatorio_campanhas():
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_campanhas"))
db = get_db_connection()
campanhas = db.query(CampanhaFinanceira).filter(
CampanhaFinanceira.data_campanha.between(data_inicio, data_fim)
).all()
return render_template("relatorio_campanhas.html",
campanhas=campanhas,
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_campanhas"))
return render_template("novo_relatorio_campanhas.html")
# Rota para listar relatórios de cotas
@app.route("/relatorios/cotas")
@@ -1223,401 +1282,164 @@ def create_app():
db.close()
return decorated_function(cr_id)
@app.route('/celulas/<int:celula_id>/pagamentos')
@app.route('/celulas/<int:celula_id>/comprovantes')
@require_login
def list_pagamentos_celula(celula_id):
def list_comprovantes_celula(celula_id):
@require_instance_access('celula', celula_id)
def decorated_function(celula_id):
db = get_db_connection()
try:
pagamentos = db.query(Pagamento).filter_by(celula_id=celula_id).all()
return render_template('pagamentos/list.html', pagamentos=pagamentos)
comprovantes = db.query(Comprovante).filter_by(celula_id=celula_id).all()
return render_template('comprovantes/list.html', comprovantes=comprovantes)
finally:
db.close()
return decorated_function(celula_id)
@app.route('/setores/<int:setor_id>/pagamentos')
@app.route('/setores/<int:setor_id>/comprovantes')
@require_login
def list_pagamentos_setor(setor_id):
def list_comprovantes_setor(setor_id):
@require_instance_access('setor', setor_id)
def decorated_function(setor_id):
db = get_db_connection()
try:
pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.setor_id == setor_id).all()
return render_template('pagamentos/list.html', pagamentos=pagamentos)
comprovantes = db.query(Comprovante).join(Usuario).filter(Usuario.setor_id == setor_id).all()
return render_template('comprovantes/list.html', comprovantes=comprovantes)
finally:
db.close()
return decorated_function(setor_id)
@app.route('/crs/<int:cr_id>/pagamentos')
@app.route('/crs/<int:cr_id>/comprovantes')
@require_login
def list_pagamentos_cr(cr_id):
def list_comprovantes_cr(cr_id):
@require_instance_access('cr', cr_id)
def decorated_function(cr_id):
db = get_db_connection()
try:
pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.cr_id == cr_id).all()
return render_template('pagamentos/list.html', pagamentos=pagamentos)
comprovantes = db.query(Comprovante).join(Usuario).filter(Usuario.cr_id == cr_id).all()
return render_template('comprovantes/list.html', comprovantes=comprovantes)
finally:
db.close()
return decorated_function(cr_id)
@app.route('/celulas/<int:celula_id>/pagamentos/novo', methods=['GET', 'POST'])
@app.route('/celulas/<int:celula_id>/comprovantes/novo', methods=['GET', 'POST'])
@require_login
@require_instance_permission('REGISTER_CELL_PAYMENT', 'celula_id')
def novo_pagamento_celula(celula_id):
@require_instance_permission('REGISTER_CELL_RECEIPT', 'celula_id')
def novo_comprovante_celula(celula_id):
if request.method == 'POST':
db = get_db_connection()
try:
pagamento = Pagamento(
comprovante = Comprovante(
valor=request.form['valor'],
data=request.form['data'],
militante_id=request.form['militante_id'],
celula_id=celula_id
)
db.add(pagamento)
db.add(comprovante)
db.commit()
flash('Pagamento registrado com sucesso!', 'success')
return redirect(url_for('list_pagamentos_celula', celula_id=celula_id))
flash('Comprovante registrado com sucesso!', 'success')
return redirect(url_for('list_comprovantes_celula', celula_id=celula_id))
finally:
db.close()
return render_template('pagamentos/form.html')
return render_template('comprovantes/form.html')
@app.route('/setores/<int:setor_id>/pagamentos/novo', methods=['GET', 'POST'])
@app.route('/setores/<int:setor_id>/comprovantes/novo', methods=['GET', 'POST'])
@require_login
@require_instance_permission('REGISTER_SECTOR_PAYMENT', 'setor_id')
def novo_pagamento_setor(setor_id):
@require_instance_permission('REGISTER_SECTOR_RECEIPT', 'setor_id')
def novo_comprovante_setor(setor_id):
if request.method == 'POST':
db = get_db_connection()
try:
pagamento = Pagamento(
comprovante = Comprovante(
valor=request.form['valor'],
data=request.form['data'],
militante_id=request.form['militante_id'],
setor_id=setor_id
)
db.add(pagamento)
db.add(comprovante)
db.commit()
flash('Pagamento registrado com sucesso!', 'success')
return redirect(url_for('list_pagamentos_setor', setor_id=setor_id))
flash('Comprovante registrado com sucesso!', 'success')
return redirect(url_for('list_comprovantes_setor', setor_id=setor_id))
finally:
db.close()
return render_template('pagamentos/form.html')
return render_template('comprovantes/form.html')
@app.route('/crs/<int:cr_id>/pagamentos/novo', methods=['GET', 'POST'])
@app.route('/crs/<int:cr_id>/comprovantes/novo', methods=['GET', 'POST'])
@require_login
@require_instance_permission('REGISTER_CR_PAYMENT', 'cr_id')
def novo_pagamento_cr(cr_id):
@require_instance_permission('REGISTER_CR_RECEIPT', 'cr_id')
def novo_comprovante_cr(cr_id):
if request.method == 'POST':
db = get_db_connection()
try:
pagamento = Pagamento(
comprovante = Comprovante(
valor=request.form['valor'],
data=request.form['data'],
militante_id=request.form['militante_id'],
cr_id=cr_id
)
db.add(pagamento)
db.add(comprovante)
db.commit()
flash('Pagamento registrado com sucesso!', 'success')
return redirect(url_for('list_pagamentos_cr', cr_id=cr_id))
flash('Comprovante registrado com sucesso!', 'success')
return redirect(url_for('list_comprovantes_cr', cr_id=cr_id))
finally:
db.close()
return render_template('pagamentos/form.html')
return render_template('comprovantes/form.html')
@app.route("/alterar_senha", methods=["GET", "POST"])
@require_login
def alterar_senha():
"""Rota para alterar a senha do usuário"""
if request.method == "POST":
senha_atual = request.form.get("senha_atual")
nova_senha = request.form.get("nova_senha")
confirmar_senha = request.form.get("confirmar_senha")
if not all([senha_atual, nova_senha, confirmar_senha]):
flash("Todos os campos são obrigatórios.", "error")
return redirect(url_for("alterar_senha"))
if nova_senha != confirmar_senha:
flash("As senhas não coincidem.", "error")
return redirect(url_for("alterar_senha"))
db = get_db_connection()
try:
user = db.query(Usuario).get(current_user.id)
if not user.check_password(senha_atual):
flash("Senha atual incorreta.", "error")
return redirect(url_for("alterar_senha"))
user.password_hash = generate_password_hash(nova_senha)
db.commit()
flash("Senha alterada com sucesso!", "success")
return redirect(url_for("home"))
finally:
db.close()
return render_template("alterar_senha.html")
@app.route('/usuarios/<int:user_id>/toggle_status', methods=['POST'])
@require_login
def toggle_user_status(user_id):
user = db_session.query(Usuario).get_or_404(user_id)
# Verificar permissões baseado na hierarquia
if not current_user.has_permission('system_config'):
if current_user.has_permission('manage_cr_sectors'):
# Secretário de CR só pode gerenciar membros do seu CR
if user.cr_id != current_user.cr_id:
flash('Você não tem permissão para gerenciar este usuário.', 'danger')
return redirect(url_for('dashboard_admin'))
elif current_user.has_permission('manage_sector_cells'):
# Secretário de Setor só pode gerenciar membros do seu setor
if user.setor_id != current_user.setor_id:
flash('Você não tem permissão para gerenciar este usuário.', 'danger')
return redirect(url_for('dashboard_admin'))
elif current_user.has_permission('manage_cell_members'):
# Secretário de Célula só pode gerenciar membros da sua célula
if user.celula_id != current_user.celula_id:
flash('Você não tem permissão para gerenciar este usuário.', 'danger')
return redirect(url_for('dashboard_admin'))
else:
# Militante básico não pode gerenciar ninguém
flash('Você não tem permissão para gerenciar usuários.', 'danger')
return redirect(url_for('dashboard_admin'))
user.ativo = not user.ativo
db_session.commit()
return jsonify({'success': True, 'message': 'Status do usuário alterado com sucesso!'})
@app.route('/usuarios/<int:user_id>/alterar_nivel', methods=['POST'])
@require_login
def alterar_nivel(user_id):
user = db_session.query(Usuario).get_or_404(user_id)
novo_nivel = request.json.get('nivel')
# Verificar permissões baseado na hierarquia
if not current_user.has_permission('system_config'):
if current_user.has_permission('manage_cr_sectors'):
# Secretário de CR só pode alterar níveis dentro do seu CR
if user.cr_id != current_user.cr_id:
return jsonify({'success': False, 'message': 'Você não tem permissão para alterar o nível deste usuário.'})
elif current_user.has_permission('manage_sector_cells'):
# Secretário de Setor só pode alterar níveis dentro do seu setor
if user.setor_id != current_user.setor_id:
return jsonify({'success': False, 'message': 'Você não tem permissão para alterar o nível deste usuário.'})
else:
# Outros níveis não podem alterar níveis
return jsonify({'success': False, 'message': 'Você não tem permissão para alterar níveis de usuários.'})
# Verificar se o novo nível é válido para o nível hierárquico do usuário atual
if current_user.has_permission('system_config'):
# Secretário Geral e Secretário de Organização podem alterar para qualquer nível
pass
elif current_user.has_permission('manage_cr_sectors'):
# Secretário de CR só pode alterar para níveis do CR
if novo_nivel not in ['membro_cr', 'secretario_cr']:
return jsonify({'success': False, 'message': 'Nível inválido para este CR.'})
elif current_user.has_permission('manage_sector_cells'):
# Secretário de Setor só pode alterar para níveis do setor
if novo_nivel not in ['membro_setor', 'secretario_setor']:
return jsonify({'success': False, 'message': 'Nível inválido para este setor.'})
# Atualizar o nível do usuário
user.role = novo_nivel
db_session.commit()
return jsonify({'success': True, 'message': 'Nível do usuário alterado com sucesso!'})
@app.route('/usuarios/<int:user_id>/toggle_quadro_orientador', methods=['POST'])
@require_login
def toggle_quadro_orientador(user_id):
db = get_db_connection()
try:
user = db.query(Usuario).get(user_id)
if not user:
return jsonify({'success': False, 'message': 'Usuário não encontrado'}), 404
# Verificar permissões
if not (current_user.has_permission('system_config') or
(current_user.has_permission('manage_cr_sectors') and user.cr_id == current_user.cr_id) or
(current_user.has_permission('manage_sector_cells') and user.setor_id == current_user.setor_id)):
return jsonify({'success': False, 'message': 'Você não tem permissão para alterar esta responsabilidade'}), 403
# Verificar se o usuário tem um militante associado
if not user.militante:
return jsonify({'success': False, 'message': 'Usuário não tem um militante associado'}), 400
# Alternar o status de Quadro-Orientador
user.militante.quadro_orientador = not user.militante.quadro_orientador
# Atualizar a responsabilidade no campo responsabilidades
if user.militante.quadro_orientador:
user.militante.responsabilidades |= Militante.QUADRO_ORIENTADOR
else:
user.militante.responsabilidades &= ~Militante.QUADRO_ORIENTADOR
db.commit()
return jsonify({
'success': True,
'message': f'Responsabilidade de Quadro-Orientador {"adicionada" if user.militante.quadro_orientador else "removida"} com sucesso'
})
except Exception as e:
db.rollback()
return jsonify({'success': False, 'message': str(e)}), 500
finally:
db.close()
@app.route("/cotas/excluir/<int:id>", methods=["POST"])
@app.route('/dashboard')
@login_required
@session_timeout
def excluir_cota(id):
"""Exclui uma cota mensal"""
db = get_db_connection()
def dashboard():
"""Rota para a página inicial do dashboard"""
try:
cota = db.query(CotaMensal).get(id)
if not cota:
flash('Cota não encontrada.', 'danger')
return redirect(url_for('listar_cotas'))
session = get_db_connection()
# Buscar dados recentes
comprovantes_recentes = session.query(Comprovante).order_by(Comprovante.data_comprovante.desc()).limit(5).all()
tipos_comprovante = session.query(TipoComprovante).all()
# Excluir a cota
db.delete(cota)
db.commit()
flash('Cota excluída com sucesso!', 'success')
except Exception as e:
db.rollback()
flash('Erro ao excluir cota. Por favor, tente novamente.', 'danger')
print(f"Erro ao excluir cota: {e}")
finally:
db.close()
return redirect(url_for('listar_cotas'))
@app.route("/assinaturas")
@require_login
@require_permission(Permission.MANAGE_CELL_REPORTS)
def listar_assinaturas():
db = get_db_connection()
try:
assinaturas = db.query(AssinaturaAnual).join(Militante).all()
militantes = db.query(Militante).all()
return render_template('listar_assinaturas.html',
assinaturas=assinaturas,
militantes=militantes)
finally:
db.close()
@app.route("/assinaturas/novo", methods=['POST'])
@require_login
@require_permission(Permission.MANAGE_CELL_REPORTS)
def nova_assinatura():
db = get_db_connection()
try:
data = request.form
# Validar dados
if not all(k in data for k in ['militante_id', 'data_inicio', 'data_fim', 'valor']):
return jsonify({'success': False, 'message': 'Todos os campos são obrigatórios'})
# Criar nova assinatura
assinatura = AssinaturaAnual(
militante_id=data['militante_id'],
data_inicio=datetime.strptime(data['data_inicio'], '%Y-%m-%d'),
data_fim=datetime.strptime(data['data_fim'], '%Y-%m-%d'),
valor=float(data['valor'])
return render_template(
'dashboard.html',
comprovantes_recentes=comprovantes_recentes,
tipos_comprovante=tipos_comprovante
)
except Exception as e:
flash(f'Erro ao carregar dashboard: {str(e)}', 'error')
return redirect(url_for('index'))
finally:
session.close()
db.add(assinatura)
db.commit()
@app.route('/comprovantes')
@login_required
@require_permission(Permission.MANAGE_MATERIALS)
def listar_comprovantes():
try:
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:
db.close()
@app.route('/comprovantes/<int:id>', methods=['DELETE'])
@login_required
@require_permission(Permission.MANAGE_MATERIALS)
def excluir_comprovante(id):
try:
comprovante = Comprovante.query.get_or_404(id)
db.session.delete(comprovante)
db.session.commit()
return jsonify({'success': True})
except Exception as e:
db.rollback()
db.session.rollback()
return jsonify({'success': False, 'message': str(e)})
finally:
db.close()
@app.route("/assinaturas/excluir/<int:id>", methods=['POST'])
@require_login
@require_permission(Permission.MANAGE_CELL_REPORTS)
def excluir_assinatura(id):
db = get_db_connection()
try:
assinatura = db.query(AssinaturaAnual).get(id)
if not assinatura:
return jsonify({'success': False, 'message': 'Assinatura não encontrada'})
db.delete(assinatura)
db.commit()
return jsonify({'success': True})
except Exception as e:
db.rollback()
return jsonify({'success': False, 'message': str(e)})
finally:
db.close()
@app.route("/militantes/dados/<int:militante_id>")
@require_login
@require_permission('gerenciar_militantes')
def buscar_dados_militante(militante_id):
"""Busca os dados de um militante específico"""
db = get_db_connection()
try:
militante = db.query(Militante).get(militante_id)
if not militante:
print(f"Militante não encontrado: ID {militante_id}")
return jsonify({
'status': 'error',
'message': 'Militante não encontrado'
}), 404
# Função auxiliar para formatar data com validação
def formatar_data_segura(data):
try:
if not data:
return None
return data.strftime('%Y-%m-%d')
except Exception as e:
print(f"Erro ao formatar data: {str(e)}, valor: {data}")
return None
# Formatar datas com validação
data_nascimento = formatar_data_segura(militante.data_nascimento)
data_entrada_oci = formatar_data_segura(militante.data_entrada_oci)
data_efetivacao_oci = formatar_data_segura(militante.data_efetivacao_oci)
print(f"Dados do militante {militante_id} recuperados com sucesso")
print(f"Datas formatadas: nascimento={data_nascimento}, entrada={data_entrada_oci}, efetivação={data_efetivacao_oci}")
return jsonify({
'status': 'success',
'id': militante.id,
'nome': militante.nome,
'cpf': militante.cpf,
'titulo_eleitoral': militante.titulo_eleitoral,
'data_nascimento': data_nascimento,
'data_entrada_oci': data_entrada_oci,
'data_efetivacao_oci': data_efetivacao_oci,
'emails': [email.endereco_email for email in militante.emails] if militante.emails else [],
'telefone1': militante.telefone1,
'telefone2': militante.telefone2,
'celula_id': militante.celula_id,
'responsabilidades_valor': militante.responsabilidades,
'sindicato': militante.sindicato,
'cargo_sindical': militante.cargo_sindical,
'central_sindical': militante.central_sindical,
'dirigente_sindical': militante.dirigente_sindical
})
except Exception as e:
import traceback
print(f"Erro ao buscar dados do militante {militante_id}:")
print(traceback.format_exc())
return jsonify({
'status': 'error',
'message': f'Erro ao buscar dados do militante: {str(e)}'
}), 500
finally:
db.close()
return app
@@ -1687,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
@@ -1695,6 +1521,6 @@ app = main()
if __name__ == '__main__':
app.run(
host='0.0.0.0',
port=5000,
debug=os.getenv('FLASK_ENV') == 'development'
port=int(os.getenv('FLASK_PORT', 5000)),
debug=True
)

View File

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

65
create_test_users.py Normal file
View File

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

View File

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

View File

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

View File

@@ -14,6 +14,9 @@ from flask_login import UserMixin
from .rbac import Role, Permission, role_permissions, user_roles
from .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'
@@ -190,6 +193,7 @@ class Militante(Base):
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
assinaturas = relationship("AssinaturaAnual", back_populates="militante")
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
comprovantes = relationship("Comprovante", back_populates="militante")
# Constantes para responsabilidades
SECRETARIO = 1
@@ -328,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'
@@ -607,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'
@@ -617,9 +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")
comprovante = relationship("Comprovante", back_populates="transacoes_pix")
def init_database():
"""Inicializa o banco de dados com dados básicos"""
@@ -660,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()
@@ -681,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")
# 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")
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')
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(provisioning_uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(qr_path)
print("=== Usuário Admin Criado ===")
print(f"Username: admin")
print(f"Senha: admin123")
print(f"Email: {admin.email}")
print(f"OTP Secret: {admin_otp_secret}")
print(f"QR Code: admin_qr.png")
print(f"QR Code: {qr_path}")
# Importar e executar o seed após criar todas as dependências
from seed_data import seed_database

View File

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

23
functions/usuario.py Normal file
View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,7 @@
</div>
<div class="form-floating mb-4">
<input type="text" class="form-control" id="otp" name="otp" placeholder="Código OTP" required>
<input type="text" class="form-control" id="otp" name="otp" placeholder="Código OTP">
<label for="otp">Código OTP</label>
<div class="invalid-feedback">
Por favor, informe o código OTP.

View File

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

View File

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

View File

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

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