feat: implementa sistema de comprovantes com centralizações e PIX
This commit is contained in:
18
Makefile
18
Makefile
@@ -1,7 +1,19 @@
|
||||
.PHONY: install run test clean refresh
|
||||
|
||||
install:
|
||||
pip install -r requirements.txt
|
||||
pip install pytest pytest-cov
|
||||
|
||||
clean:
|
||||
find . -type d -name "__pycache__" -exec rm -r {} +
|
||||
find . -type f -name "*.pyc" -delete
|
||||
find . -type f -name "*.pyo" -delete
|
||||
find . -type f -name "*.pyd" -delete
|
||||
find . -type f -name ".coverage" -delete
|
||||
find . -type d -name "*.egg-info" -exec rm -r {} +
|
||||
find . -type d -name "*.egg" -exec rm -r {} +
|
||||
find . -type d -name ".pytest_cache" -exec rm -r {} +
|
||||
find . -type d -name "htmlcov" -exec rm -r {} +
|
||||
rm -rf ~/.local/share/controles/database.db*
|
||||
rm -f admin_qr.png
|
||||
|
||||
@@ -18,3 +30,9 @@ run-with-seed: seed run
|
||||
|
||||
reset-admin: clean
|
||||
python create_admin.py
|
||||
|
||||
test:
|
||||
pytest tests/ --cov=app --cov=functions --cov-report=term-missing
|
||||
|
||||
refresh: clean install test
|
||||
python app.py
|
||||
|
||||
319
app.py
319
app.py
@@ -24,14 +24,14 @@ from functions.database import (
|
||||
Endereco,
|
||||
TipoComprovante,
|
||||
Comprovante,
|
||||
VendaJornal,
|
||||
AssinaturaJornal,
|
||||
CampanhaFinanceira,
|
||||
TransacaoPIX,
|
||||
Permission,
|
||||
Role,
|
||||
RolePermission,
|
||||
UserRole
|
||||
Atividade,
|
||||
MaterialAtividade,
|
||||
Relatorio,
|
||||
CentralizacaoComprovante
|
||||
)
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, joinedload
|
||||
@@ -73,10 +73,6 @@ def create_app():
|
||||
csrf = CSRFProtect()
|
||||
csrf.init_app(app)
|
||||
|
||||
# Configurar cabeçalhos CSRF personalizados
|
||||
app.config['WTF_CSRF_CHECK_DEFAULT'] = False
|
||||
app.config['WTF_CSRF_HEADERS'] = ['X-CSRFToken']
|
||||
|
||||
# Configurar Flask-Login
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
@@ -88,6 +84,13 @@ def create_app():
|
||||
"""Filtro para operação bit a bit AND"""
|
||||
return value1 & value2
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
"""Configurações antes de cada requisição"""
|
||||
session.permanent = True
|
||||
app.permanent_session_lifetime = timedelta(minutes=30)
|
||||
session.modified = True
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
"""Carrega o usuário pelo ID"""
|
||||
@@ -214,14 +217,15 @@ def create_app():
|
||||
flash("Email/usuário ou senha incorretos.", "danger")
|
||||
return redirect(url_for("login"))
|
||||
|
||||
# Verificar OTP se o usuário tiver configurado
|
||||
if user.otp_secret and not otp:
|
||||
flash("Código OTP é obrigatório para sua conta.", "danger")
|
||||
return redirect(url_for("login"))
|
||||
# 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()
|
||||
@@ -282,27 +286,22 @@ def create_app():
|
||||
.limit(5)\
|
||||
.all()
|
||||
|
||||
# Buscar últimos comprovantes
|
||||
ultimos_comprovantes = db.query(Comprovante)\
|
||||
.join(Militante)\
|
||||
.order_by(Comprovante.data_comprovante.desc())\
|
||||
# Buscar últimos pagamentos
|
||||
ultimos_pagamentos = db.query(Pagamento)\
|
||||
.order_by(Pagamento.data_pagamento.desc())\
|
||||
.limit(5)\
|
||||
.all()
|
||||
|
||||
# Buscar tipos de comprovante
|
||||
tipos_comprovante = db.query(TipoComprovante).all()
|
||||
|
||||
return render_template('home.html',
|
||||
nome_usuario=nome_usuario,
|
||||
data_atual=data_atual,
|
||||
total_militantes=total_militantes,
|
||||
total_cotas="{:.2f}".format(total_cotas),
|
||||
total_materiais=total_materiais,
|
||||
total_assinaturas=total_assinaturas,
|
||||
ultimos_militantes=ultimos_militantes,
|
||||
ultimos_comprovantes=ultimos_comprovantes,
|
||||
tipos_comprovante=tipos_comprovante,
|
||||
user=current_user)
|
||||
nome_usuario=nome_usuario,
|
||||
data_atual=data_atual,
|
||||
total_militantes=total_militantes,
|
||||
total_cotas=total_cotas,
|
||||
total_materiais=total_materiais,
|
||||
total_assinaturas=total_assinaturas,
|
||||
ultimos_militantes=ultimos_militantes,
|
||||
ultimos_pagamentos=ultimos_pagamentos,
|
||||
Militante=Militante)
|
||||
except Exception as e:
|
||||
print(f"Erro na página inicial: {e}")
|
||||
import traceback
|
||||
@@ -316,7 +315,7 @@ def create_app():
|
||||
total_materiais=0,
|
||||
total_assinaturas=0,
|
||||
ultimos_militantes=[],
|
||||
ultimos_comprovantes=[],
|
||||
ultimos_pagamentos=[],
|
||||
Militante=Militante)
|
||||
finally:
|
||||
db.close()
|
||||
@@ -819,7 +818,7 @@ def create_app():
|
||||
return redirect(url_for("nova_venda_jornal"))
|
||||
|
||||
db = get_db_connection()
|
||||
venda_jornal = VendaJornal(
|
||||
venda_jornal = VendaJornalAvulso(
|
||||
militante_id=militante_id,
|
||||
quantidade=quantidade,
|
||||
data_venda=data_venda
|
||||
@@ -842,7 +841,7 @@ def create_app():
|
||||
@require_permission(Permission.MANAGE_MATERIALS)
|
||||
def listar_vendas_jornal():
|
||||
db = get_db_connection()
|
||||
vendas_jornal = db.query(VendaJornal).all()
|
||||
vendas_jornal = db.query(VendaJornalAvulso).all()
|
||||
return render_template("listar_vendas_jornal.html", vendas_jornal=vendas_jornal)
|
||||
|
||||
# Rota para criar um novo relatório de cotas
|
||||
@@ -888,8 +887,8 @@ def create_app():
|
||||
return redirect(url_for("novo_relatorio_jornais"))
|
||||
|
||||
db = get_db_connection()
|
||||
jornais = db.query(VendaJornal).filter(
|
||||
VendaJornal.data_venda.between(data_inicio, data_fim)
|
||||
jornais = db.query(VendaJornalAvulso).filter(
|
||||
VendaJornalAvulso.data_venda.between(data_inicio, data_fim)
|
||||
).all()
|
||||
|
||||
return render_template("relatorio_jornais.html",
|
||||
@@ -916,8 +915,8 @@ def create_app():
|
||||
return redirect(url_for("novo_relatorio_assinaturas"))
|
||||
|
||||
db = get_db_connection()
|
||||
assinaturas = db.query(AssinaturaJornal).filter(
|
||||
AssinaturaJornal.data_assinatura.between(data_inicio, data_fim)
|
||||
assinaturas = db.query(AssinaturaAnual).filter(
|
||||
AssinaturaAnual.data_assinatura.between(data_inicio, data_fim)
|
||||
).all()
|
||||
|
||||
return render_template("relatorio_assinaturas.html",
|
||||
@@ -1406,227 +1405,41 @@ def create_app():
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@app.route('/novo_comprovante', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def novo_comprovante():
|
||||
"""Rota para criar um novo comprovante"""
|
||||
try:
|
||||
session = get_db_connection()
|
||||
if request.method == 'POST':
|
||||
militante_id = request.form.get('militante_id')
|
||||
tipo_comprovante = request.form.get('tipo_comprovante')
|
||||
valor = request.form.get('valor')
|
||||
data_comprovante = request.form.get('data_comprovante')
|
||||
|
||||
if not all([militante_id, tipo_comprovante, valor, data_comprovante]):
|
||||
flash('Todos os campos são obrigatórios', 'error')
|
||||
return redirect(url_for('novo_comprovante'))
|
||||
|
||||
comprovante = Comprovante(
|
||||
militante_id=militante_id,
|
||||
tipo_comprovante=tipo_comprovante,
|
||||
valor=float(valor.replace('R$', '').replace('.', '').replace(',', '.')),
|
||||
data_comprovante=datetime.strptime(data_comprovante, '%Y-%m-%d')
|
||||
)
|
||||
|
||||
session.add(comprovante)
|
||||
session.commit()
|
||||
flash('Comprovante criado com sucesso!', 'success')
|
||||
return redirect(url_for('listar_comprovantes'))
|
||||
|
||||
militantes = session.query(Militante).all()
|
||||
tipos_comprovante = session.query(TipoComprovante).all()
|
||||
return render_template(
|
||||
'novo_comprovante.html',
|
||||
militantes=militantes,
|
||||
tipos_comprovante=tipos_comprovante
|
||||
)
|
||||
except Exception as e:
|
||||
flash(f'Erro ao criar comprovante: {str(e)}', 'error')
|
||||
return redirect(url_for('listar_comprovantes'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@app.route('/listar_comprovantes')
|
||||
@app.route('/comprovantes')
|
||||
@login_required
|
||||
@require_permission(Permission.MANAGE_MATERIALS)
|
||||
def listar_comprovantes():
|
||||
"""Rota para listar todos os comprovantes"""
|
||||
try:
|
||||
session = get_db_connection()
|
||||
comprovantes = session.query(Comprovante).all()
|
||||
return render_template('listar_comprovantes.html', comprovantes=comprovantes)
|
||||
db = get_db_connection()
|
||||
comprovantes = db.query(Comprovante)\
|
||||
.options(joinedload(Comprovante.centralizacoes))\
|
||||
.options(joinedload(Comprovante.militante))\
|
||||
.options(joinedload(Comprovante.campanha))\
|
||||
.all()
|
||||
militantes = db.query(Militante).order_by(Militante.nome).all()
|
||||
campanhas = db.query(CampanhaFinanceira).all()
|
||||
return render_template('listar_comprovantes.html',
|
||||
comprovantes=comprovantes,
|
||||
militantes=militantes,
|
||||
campanhas=campanhas)
|
||||
except Exception as e:
|
||||
flash(f'Erro ao listar comprovantes: {str(e)}', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
finally:
|
||||
session.close()
|
||||
db.close()
|
||||
|
||||
@app.route('/adicionar_comprovante', methods=['POST'])
|
||||
@app.route('/comprovantes/<int:id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def adicionar_comprovante():
|
||||
"""Rota para adicionar um novo comprovante via AJAX"""
|
||||
@require_permission(Permission.MANAGE_MATERIALS)
|
||||
def excluir_comprovante(id):
|
||||
try:
|
||||
session = get_db_connection()
|
||||
data = request.get_json()
|
||||
|
||||
comprovante = Comprovante(
|
||||
militante_id=data['militante_id'],
|
||||
tipo_comprovante=data['tipo_comprovante'],
|
||||
valor=float(data['valor'].replace('R$', '').replace('.', '').replace(',', '.')),
|
||||
data_comprovante=datetime.strptime(data['data_comprovante'], '%Y-%m-%d')
|
||||
)
|
||||
|
||||
session.add(comprovante)
|
||||
session.commit()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Comprovante adicionado com sucesso!'
|
||||
})
|
||||
comprovante = Comprovante.query.get_or_404(id)
|
||||
db.session.delete(comprovante)
|
||||
db.session.commit()
|
||||
return jsonify({'success': True})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Erro ao adicionar comprovante: {str(e)}'
|
||||
})
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@app.route("/relatorios/comprovantes/novo", methods=["GET", "POST"])
|
||||
@require_login
|
||||
@require_permission(Permission.MANAGE_CELL_REPORTS)
|
||||
def novo_relatorio_comprovantes():
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data_inicio = request.form.get("data_inicio")
|
||||
data_fim = request.form.get("data_fim")
|
||||
|
||||
if not all([data_inicio, data_fim]):
|
||||
flash("Todos os campos são obrigatórios", "danger")
|
||||
return redirect(url_for("novo_relatorio_comprovantes"))
|
||||
|
||||
db = get_db_connection()
|
||||
comprovantes = db.query(Comprovante).filter(
|
||||
Comprovante.data_comprovante.between(data_inicio, data_fim)
|
||||
).all()
|
||||
|
||||
return render_template("relatorio_comprovantes.html",
|
||||
comprovantes=comprovantes,
|
||||
data_inicio=data_inicio,
|
||||
data_fim=data_fim)
|
||||
except Exception as e:
|
||||
flash(f"Erro ao gerar relatório: {str(e)}", "danger")
|
||||
return redirect(url_for("novo_relatorio_comprovantes"))
|
||||
return render_template("novo_relatorio_comprovantes.html")
|
||||
|
||||
@app.route("/relatorios/comprovantes/celula/<int:celula_id>", methods=["GET", "POST"])
|
||||
@require_login
|
||||
@require_permission(Permission.MANAGE_CELL_REPORTS)
|
||||
def relatorio_comprovantes_celula(celula_id):
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data_inicio = request.form.get("data_inicio")
|
||||
data_fim = request.form.get("data_fim")
|
||||
|
||||
if not all([data_inicio, data_fim]):
|
||||
flash("Todos os campos são obrigatórios", "danger")
|
||||
return redirect(url_for("relatorio_comprovantes_celula", celula_id=celula_id))
|
||||
|
||||
db = get_db_connection()
|
||||
comprovantes = db.query(Comprovante).join(Militante).filter(
|
||||
Militante.celula_id == celula_id,
|
||||
Comprovante.data_comprovante.between(data_inicio, data_fim)
|
||||
).all()
|
||||
|
||||
return render_template("relatorio_comprovantes.html",
|
||||
comprovantes=comprovantes,
|
||||
data_inicio=data_inicio,
|
||||
data_fim=data_fim)
|
||||
except Exception as e:
|
||||
flash(f"Erro ao gerar relatório: {str(e)}", "danger")
|
||||
return redirect(url_for("relatorio_comprovantes_celula", celula_id=celula_id))
|
||||
return render_template("novo_relatorio_comprovantes.html", celula_id=celula_id)
|
||||
|
||||
@app.route("/assinaturas/jornal/novo", methods=["GET", "POST"])
|
||||
@require_login
|
||||
@require_permission(Permission.MANAGE_MATERIALS)
|
||||
def nova_assinatura_jornal():
|
||||
if request.method == "POST":
|
||||
try:
|
||||
militante_id = request.form.get("militante_id")
|
||||
data_inicio = request.form.get("data_inicio")
|
||||
data_fim = request.form.get("data_fim")
|
||||
|
||||
if not all([militante_id, data_inicio, data_fim]):
|
||||
flash("Todos os campos são obrigatórios", "danger")
|
||||
return redirect(url_for("nova_assinatura_jornal"))
|
||||
|
||||
db = get_db_connection()
|
||||
assinatura = AssinaturaJornal(
|
||||
militante_id=militante_id,
|
||||
data_inicio=data_inicio,
|
||||
data_fim=data_fim
|
||||
)
|
||||
db.add(assinatura)
|
||||
db.commit()
|
||||
|
||||
flash("Assinatura de jornal registrada com sucesso", "success")
|
||||
return redirect(url_for("listar_assinaturas_jornal"))
|
||||
except Exception as e:
|
||||
flash(f"Erro ao registrar assinatura de jornal: {str(e)}", "danger")
|
||||
return redirect(url_for("nova_assinatura_jornal"))
|
||||
|
||||
db = get_db_connection()
|
||||
militantes = db.query(Militante).all()
|
||||
return render_template("nova_assinatura_jornal.html", militantes=militantes)
|
||||
|
||||
@app.route("/assinaturas/jornal")
|
||||
@require_login
|
||||
@require_permission(Permission.MANAGE_MATERIALS)
|
||||
def listar_assinaturas_jornal():
|
||||
db = get_db_connection()
|
||||
assinaturas = db.query(AssinaturaJornal).all()
|
||||
return render_template("listar_assinaturas_jornal.html", assinaturas=assinaturas)
|
||||
|
||||
@app.route("/campanhas/financeira/novo", methods=["GET", "POST"])
|
||||
@require_login
|
||||
@require_permission(Permission.MANAGE_MATERIALS)
|
||||
def nova_campanha_financeira():
|
||||
if request.method == "POST":
|
||||
try:
|
||||
militante_id = request.form.get("militante_id")
|
||||
valor = request.form.get("valor")
|
||||
data_campanha = request.form.get("data_campanha")
|
||||
|
||||
if not all([militante_id, valor, data_campanha]):
|
||||
flash("Todos os campos são obrigatórios", "danger")
|
||||
return redirect(url_for("nova_campanha_financeira"))
|
||||
|
||||
db = get_db_connection()
|
||||
campanha = CampanhaFinanceira(
|
||||
militante_id=militante_id,
|
||||
valor=valor,
|
||||
data_campanha=data_campanha
|
||||
)
|
||||
db.add(campanha)
|
||||
db.commit()
|
||||
|
||||
flash("Campanha financeira registrada com sucesso", "success")
|
||||
return redirect(url_for("listar_campanhas_financeira"))
|
||||
except Exception as e:
|
||||
flash(f"Erro ao registrar campanha financeira: {str(e)}", "danger")
|
||||
return redirect(url_for("nova_campanha_financeira"))
|
||||
|
||||
db = get_db_connection()
|
||||
militantes = db.query(Militante).all()
|
||||
return render_template("nova_campanha_financeira.html", militantes=militantes)
|
||||
|
||||
@app.route("/campanhas/financeira")
|
||||
@require_login
|
||||
@require_permission(Permission.MANAGE_MATERIALS)
|
||||
def listar_campanhas_financeira():
|
||||
db = get_db_connection()
|
||||
campanhas = db.query(CampanhaFinanceira).all()
|
||||
return render_template("listar_campanhas_financeira.html", campanhas=campanhas)
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': str(e)})
|
||||
|
||||
return app
|
||||
|
||||
@@ -1696,6 +1509,10 @@ def main():
|
||||
# Inicializar o sistema
|
||||
init_system()
|
||||
|
||||
# Configurar modo debug
|
||||
app.debug = True
|
||||
app.config['DEBUG'] = True
|
||||
|
||||
return app
|
||||
|
||||
# Criar a aplicação usando a função main
|
||||
@@ -1704,6 +1521,6 @@ app = main()
|
||||
if __name__ == '__main__':
|
||||
app.run(
|
||||
host='0.0.0.0',
|
||||
port=5000,
|
||||
debug=os.getenv('FLASK_ENV') == 'development'
|
||||
port=int(os.getenv('FLASK_PORT', 5000)),
|
||||
debug=True
|
||||
)
|
||||
|
||||
119
create_admin.py
119
create_admin.py
@@ -41,102 +41,37 @@ def generate_qr_code(user):
|
||||
return qr_path, otp_uri
|
||||
|
||||
def create_admin_user():
|
||||
"""Cria ou atualiza o usuário admin"""
|
||||
"""Cria o usuário admin do sistema"""
|
||||
session = get_db_connection()
|
||||
try:
|
||||
# Inicializar banco de dados
|
||||
init_database()
|
||||
# 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
65
create_test_users.py
Normal 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()
|
||||
@@ -14,6 +14,9 @@ from flask_login import UserMixin
|
||||
from .rbac import Role, Permission, role_permissions, user_roles
|
||||
from .base import Base, engine, Session
|
||||
import logging
|
||||
import qrcode
|
||||
from PIL import Image
|
||||
import re
|
||||
|
||||
# Configurar caminho do banco de dados
|
||||
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
||||
@@ -329,7 +332,6 @@ class Pagamento(Base):
|
||||
data_pagamento = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="pagamentos")
|
||||
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")
|
||||
|
||||
class TipoMaterial(Base):
|
||||
__tablename__ = 'tipos_materiais'
|
||||
@@ -608,6 +610,49 @@ class Relatorio(Base):
|
||||
setor = relationship("Setor", foreign_keys=[setor_id])
|
||||
cr = relationship("ComiteRegional", foreign_keys=[cr_id])
|
||||
|
||||
class CampanhaFinanceira(Base):
|
||||
__tablename__ = 'campanhas_financeiras'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
nome = Column(String(100), nullable=False)
|
||||
descricao = Column(Text)
|
||||
data_inicio = Column(Date, nullable=False)
|
||||
data_fim = Column(Date, nullable=False)
|
||||
meta = Column(Numeric(10, 2), nullable=False)
|
||||
valor_arrecadado = Column(Numeric(10, 2), default=0)
|
||||
status = Column(String(20), default='Em andamento') # Em andamento, Concluída, Cancelada
|
||||
|
||||
comprovantes = relationship("Comprovante", back_populates="campanha")
|
||||
|
||||
class TipoComprovante(Base):
|
||||
__tablename__ = 'tipos_comprovante'
|
||||
id = Column(Integer, primary_key=True)
|
||||
descricao = Column(String(50), nullable=False)
|
||||
valor = Column(Numeric(10, 2), nullable=False)
|
||||
|
||||
class CentralizacaoComprovante(Base):
|
||||
__tablename__ = 'centralizacoes_comprovante'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
comprovante_id = Column(Integer, ForeignKey('comprovantes.id'), nullable=False)
|
||||
tipo_comprovante = Column(String(50), nullable=False) # Cota, Jornal, Assinatura, etc.
|
||||
valor = Column(Numeric(10, 2), nullable=False)
|
||||
|
||||
comprovante = relationship("Comprovante", back_populates="centralizacoes")
|
||||
|
||||
class Comprovante(Base):
|
||||
__tablename__ = 'comprovantes'
|
||||
id = Column(Integer, primary_key=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
|
||||
data_comprovante = Column(Date, nullable=False)
|
||||
forma_pagamento = Column(String(20), nullable=False) # PIX, transferência/DOC, depósito, maquininha
|
||||
campanha_id = Column(Integer, ForeignKey('campanhas_financeiras.id'))
|
||||
|
||||
militante = relationship("Militante", back_populates="comprovantes")
|
||||
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")
|
||||
campanha = relationship("CampanhaFinanceira", back_populates="comprovantes")
|
||||
centralizacoes = relationship("CentralizacaoComprovante", back_populates="comprovante", cascade="all, delete-orphan")
|
||||
|
||||
class TransacaoPIX(Base):
|
||||
__tablename__ = 'transacoes_pix'
|
||||
|
||||
@@ -618,25 +663,9 @@ class TransacaoPIX(Base):
|
||||
data_pagamento = Column(DateTime)
|
||||
status = Column(String(20)) # Pendente, Pago, Expirado
|
||||
qr_code = Column(Text)
|
||||
pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
|
||||
comprovante_id = Column(Integer, ForeignKey('comprovantes.id'))
|
||||
|
||||
pagamento = relationship("Pagamento", back_populates="transacoes_pix")
|
||||
|
||||
class TipoComprovante(Base):
|
||||
__tablename__ = 'tipos_comprovante'
|
||||
id = Column(Integer, primary_key=True)
|
||||
descricao = Column(String(50), nullable=False)
|
||||
valor = Column(Float, nullable=False)
|
||||
|
||||
class Comprovante(Base):
|
||||
__tablename__ = 'comprovantes'
|
||||
id = Column(Integer, primary_key=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
|
||||
tipo_comprovante = Column(String(50)) # Cota, Jornal, Assinatura, etc.
|
||||
data_comprovante = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="comprovantes")
|
||||
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")
|
||||
comprovante = relationship("Comprovante", back_populates="transacoes_pix")
|
||||
|
||||
def init_database():
|
||||
"""Inicializa o banco de dados com dados básicos"""
|
||||
@@ -677,9 +706,30 @@ def init_database():
|
||||
session.add(comite)
|
||||
session.commit()
|
||||
|
||||
# Gerar OTP para admin
|
||||
admin_otp_secret = pyotp.random_base32()
|
||||
print(f"Novo OTP gerado: {admin_otp_secret}")
|
||||
# Verificar se existe QR code do admin
|
||||
admin_otp_secret = None
|
||||
qr_path = 'admin_qr.png'
|
||||
|
||||
if os.path.exists(qr_path):
|
||||
try:
|
||||
# Tentar ler o QR code existente
|
||||
from pyzbar.pyzbar import decode
|
||||
qr_data = decode(Image.open(qr_path))
|
||||
if qr_data:
|
||||
# O URI do OTP está no formato: otpauth://totp/Sistema%20de%20Controles:admin?secret=XXXXX&issuer=Sistema%20de%20Controles
|
||||
uri = qr_data[0].data.decode('utf-8')
|
||||
# Extrair o secret do URI
|
||||
match = re.search(r'secret=([A-Z0-9]+)', uri)
|
||||
if match:
|
||||
admin_otp_secret = match.group(1)
|
||||
print("OTP existente encontrado no QR code")
|
||||
except Exception as e:
|
||||
print(f"Erro ao ler QR code existente: {e}")
|
||||
|
||||
if not admin_otp_secret:
|
||||
# Se não conseguiu ler o QR code ou ele não existe, gera um novo
|
||||
admin_otp_secret = pyotp.random_base32()
|
||||
print(f"Novo OTP gerado: {admin_otp_secret}")
|
||||
|
||||
# Criar usuário admin
|
||||
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||
@@ -698,23 +748,23 @@ def init_database():
|
||||
session.add(admin)
|
||||
session.commit()
|
||||
|
||||
# Gerar QR code
|
||||
totp = pyotp.totp.TOTP(admin_otp_secret)
|
||||
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
|
||||
# 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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -8,7 +8,7 @@ Werkzeug==3.0.1
|
||||
python-dotenv==1.0.1
|
||||
pyotp==2.9.0
|
||||
qrcode==7.4.2
|
||||
Pillow==9.5.0
|
||||
Pillow==10.2.0
|
||||
email-validator==2.1.0.post1
|
||||
cryptography==42.0.2
|
||||
bcrypt==4.1.2
|
||||
@@ -17,3 +17,6 @@ flask-bootstrap5==0.1.dev1
|
||||
PyJWT==2.8.0
|
||||
gunicorn==21.2.0
|
||||
Faker==19.13.0
|
||||
pytest==8.0.0
|
||||
pytest-cov==4.1.0
|
||||
pyzbar==0.1.9
|
||||
|
||||
39
seed_data.py
39
seed_data.py
@@ -5,7 +5,7 @@ from functions.database import (
|
||||
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
|
||||
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
|
||||
ComiteRegional, Celula, EstadoMilitante, get_db_connection,
|
||||
init_database
|
||||
init_database, CentralizacaoComprovante
|
||||
)
|
||||
import random
|
||||
from faker import Faker
|
||||
@@ -59,17 +59,17 @@ def criar_tipos_comprovante(session):
|
||||
"""Cria tipos de comprovante padrão"""
|
||||
print("\nCriando tipos de comprovante...")
|
||||
tipos = [
|
||||
"Comprovante Padrão",
|
||||
"Comprovante Especial",
|
||||
"Comprovante Extraordinário",
|
||||
"Jornal Avulso",
|
||||
"Assinatura de Jornal",
|
||||
"Campanha Financeira"
|
||||
("Comprovante Padrão", 50.00),
|
||||
("Comprovante Especial", 100.00),
|
||||
("Comprovante Extraordinário", 200.00),
|
||||
("Jornal Avulso", 5.00),
|
||||
("Assinatura de Jornal", 30.00),
|
||||
("Campanha Financeira", 0.00) # Valor variável
|
||||
]
|
||||
|
||||
for tipo in tipos:
|
||||
if not session.query(TipoComprovante).filter_by(descricao=tipo).first():
|
||||
session.add(TipoComprovante(descricao=tipo))
|
||||
for descricao, valor in tipos:
|
||||
if not session.query(TipoComprovante).filter_by(descricao=descricao).first():
|
||||
session.add(TipoComprovante(descricao=descricao, valor=valor))
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
@@ -229,14 +229,25 @@ def criar_comprovantes(session, militantes):
|
||||
try:
|
||||
# Criar entre 3 e 8 comprovantes por militante
|
||||
for _ in range(random.randint(3, 8)):
|
||||
tipo = random.choice(tipos_comprovante)
|
||||
# Criar o comprovante base
|
||||
comprovante = Comprovante(
|
||||
militante_id=militante.id,
|
||||
tipo_comprovante=tipo.descricao, # Usando a descrição do tipo
|
||||
valor=random.uniform(10, 1000),
|
||||
data_comprovante=fake.date_between(start_date='-1y', end_date='today')
|
||||
data_comprovante=fake.date_between(start_date='-1y', end_date='today'),
|
||||
forma_pagamento=random.choice(['PIX', 'transferência/DOC', 'depósito', 'maquininha'])
|
||||
)
|
||||
session.add(comprovante)
|
||||
session.flush() # Para obter o ID do comprovante
|
||||
|
||||
# Criar a centralização para o comprovante
|
||||
tipo = random.choice(tipos_comprovante)
|
||||
valor = random.uniform(10, 1000)
|
||||
centralizacao = CentralizacaoComprovante(
|
||||
comprovante_id=comprovante.id,
|
||||
tipo_comprovante=tipo.descricao,
|
||||
valor=valor
|
||||
)
|
||||
session.add(centralizacao)
|
||||
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2><i class="fas fa-money-bill-wave"></i> Comprovantes</h2>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoComprovante">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#novoComprovanteModal">
|
||||
<i class="fas fa-plus"></i> Novo Comprovante
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btnExportar">
|
||||
@@ -22,40 +22,30 @@
|
||||
<table class="table table-striped table-hover" id="tabelaComprovantes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Militante</th>
|
||||
<th>Tipo de Comprovante</th>
|
||||
<th>Valor</th>
|
||||
<th>Data do Comprovante</th>
|
||||
<th>Data</th>
|
||||
<th>Forma de Pagamento</th>
|
||||
<th>Campanha</th>
|
||||
<th>Centralizações</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for comprovante in comprovantes %}
|
||||
<tr>
|
||||
<td data-militante="{{ comprovante.militante_id }}">{{ comprovante.militante.nome if comprovante.militante else 'N/A' }}</td>
|
||||
<td data-tipo="{{ comprovante.tipo_comprovante }}">
|
||||
{% if comprovante.tipo_comprovante == 1 %}
|
||||
Cota
|
||||
{% elif comprovante.tipo_comprovante == 2 %}
|
||||
Contribuição Extra
|
||||
{% elif comprovante.tipo_comprovante == 3 %}
|
||||
Doação
|
||||
{% elif comprovante.tipo_comprovante == 4 %}
|
||||
Taxa de Evento
|
||||
{% elif comprovante.tipo_comprovante == 5 %}
|
||||
Jornal Avulso
|
||||
{% elif comprovante.tipo_comprovante == 6 %}
|
||||
Assinatura de Jornal
|
||||
{% elif comprovante.tipo_comprovante == 7 %}
|
||||
Campanha Financeira
|
||||
{% elif comprovante.tipo_comprovante == 8 %}
|
||||
Outros
|
||||
{% else %}
|
||||
Não Definido
|
||||
{% endif %}
|
||||
<td>{{ comprovante.id }}</td>
|
||||
<td>{{ comprovante.militante.nome }}</td>
|
||||
<td>{{ comprovante.data_comprovante.strftime('%d/%m/%Y') }}</td>
|
||||
<td>{{ comprovante.forma_pagamento }}</td>
|
||||
<td>{{ comprovante.campanha.nome if comprovante.campanha else '-' }}</td>
|
||||
<td>
|
||||
<ul class="list-unstyled">
|
||||
{% for centralizacao in comprovante.centralizacoes %}
|
||||
<li>{{ centralizacao.tipo_comprovante }}: R$ {{ "%.2f"|format(centralizacao.valor) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
<td data-valor="{{ comprovante.valor }}">R$ {{ "%.2f"|format(comprovante.valor) }}</td>
|
||||
<td data-data="{{ comprovante.data_comprovante }}">{{ comprovante.data_comprovante.strftime('%d/%m/%Y') }}</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
@@ -63,8 +53,7 @@
|
||||
data-bs-target="#modalEditarComprovante"
|
||||
data-comprovante-id="{{ comprovante.id }}"
|
||||
data-militante-id="{{ comprovante.militante_id }}"
|
||||
data-tipo-comprovante="{{ comprovante.tipo_comprovante }}"
|
||||
data-valor="{{ comprovante.valor }}"
|
||||
data-militante-nome="{{ comprovante.militante.nome }}"
|
||||
data-data-comprovante="{{ comprovante.data_comprovante.strftime('%Y-%m-%d') }}"
|
||||
title="Editar">
|
||||
<i class="fas fa-edit"></i>
|
||||
@@ -74,7 +63,7 @@
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modalExcluirComprovante"
|
||||
data-comprovante-id="{{ comprovante.id }}"
|
||||
data-comprovante-info="Comprovante de {{ comprovante.militante.nome if comprovante.militante else 'N/A' }} - R$ {{ "%.2f"|format(comprovante.valor) }}"
|
||||
data-comprovante-info="Comprovante de {{ comprovante.militante.nome }} - Total: R$ {{ "%.2f"|format(comprovante.centralizacoes|sum(attribute='valor')) }}"
|
||||
title="Excluir">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@@ -89,58 +78,159 @@
|
||||
</div>
|
||||
|
||||
<!-- Modal Novo Comprovante -->
|
||||
<div class="modal fade" id="modalNovoComprovante" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal fade" id="novoComprovanteModal" tabindex="-1" aria-labelledby="novoComprovanteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-plus"></i> Novo Comprovante</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
<h5 class="modal-title" id="novoComprovanteModalLabel">Novo Comprovante</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="formNovoComprovante" method="post" action="{{ url_for('adicionar_comprovante') }}">
|
||||
<div class="mb-3">
|
||||
<label for="militante" class="form-label">Militante:</label>
|
||||
<select class="form-select" id="militante" name="militante_id" required>
|
||||
<option value="">Selecione um militante</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<form id="novoComprovanteForm">
|
||||
<!-- Dados únicos do comprovante -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="militante_id" class="form-label">Militante</label>
|
||||
<select class="form-select" id="militante_id" name="militante_id" required>
|
||||
<option value="">Selecione o militante</option>
|
||||
{% for militante in militantes %}
|
||||
<option value="{{ militante.id }}">{{ militante.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="data_comprovante" class="form-label">Data do Comprovante</label>
|
||||
<input type="date" class="form-control" id="data_comprovante" name="data_comprovante" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="tipoComprovante" class="form-label">Tipo de Comprovante:</label>
|
||||
<select class="form-select" id="tipoComprovante" name="tipo_comprovante" required>
|
||||
<option value="">Selecione o tipo</option>
|
||||
<option value="1">Cota</option>
|
||||
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
|
||||
<option value="2">Contribuição Extra</option>
|
||||
<option value="3">Doação</option>
|
||||
<option value="4">Taxa de Evento</option>
|
||||
<option value="5">Jornal Avulso</option>
|
||||
<option value="6">Assinatura de Jornal</option>
|
||||
<option value="7">Campanha Financeira</option>
|
||||
<option value="8">Outros</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="forma_pagamento" class="form-label">Forma de Pagamento</label>
|
||||
<select class="form-select" id="forma_pagamento" name="forma_pagamento" required>
|
||||
<option value="">Selecione a forma de pagamento</option>
|
||||
<option value="PIX">PIX</option>
|
||||
<option value="TRANSFERENCIA">Transferência/DOC</option>
|
||||
<option value="DEPOSITO">Depósito</option>
|
||||
<option value="MAQUININHA">Maquininha</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="campanha_id" class="form-label">Campanha</label>
|
||||
<select class="form-select" id="campanha_id" name="campanha_id">
|
||||
<option value="">Selecione a campanha</option>
|
||||
{% for campanha in campanhas %}
|
||||
<option value="{{ campanha.id }}">{{ campanha.nome }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="valor" class="form-label">Valor:</label>
|
||||
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dataComprovante" class="form-label">Data do Comprovante:</label>
|
||||
<input type="date" class="form-control" id="dataComprovante" name="data_comprovante" required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
|
||||
<!-- Centralizações -->
|
||||
<div class="centralizacoes-container">
|
||||
<h6 class="mb-3">Centralizações</h6>
|
||||
<div class="centralizacao-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Tipo de Comprovante</label>
|
||||
<select class="form-select tipo-comprovante" name="tipo_comprovante[]" required>
|
||||
<option value="">Selecione o tipo</option>
|
||||
<option value="COTA">Cota</option>
|
||||
<option value="JORNAL">Jornal</option>
|
||||
<option value="ASSINATURA">Assinatura</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Valor</label>
|
||||
<input type="number" class="form-control valor" name="valor[]" step="0.01" required>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-danger btn-sm remover-centralizacao">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm mb-3" id="adicionar-centralizacao">
|
||||
<i class="bi bi-plus"></i> Adicionar Centralização
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="button" class="btn btn-primary" id="salvarComprovante">Salvar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.centralizacao-item {
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Adicionar nova centralização
|
||||
document.getElementById('adicionar-centralizacao').addEventListener('click', function() {
|
||||
const container = document.querySelector('.centralizacoes-container');
|
||||
const newItem = document.querySelector('.centralizacao-item').cloneNode(true);
|
||||
newItem.querySelector('.valor').value = '';
|
||||
newItem.querySelector('.tipo-comprovante').value = '';
|
||||
container.appendChild(newItem);
|
||||
});
|
||||
|
||||
// Remover centralização
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.remover-centralizacao')) {
|
||||
const centralizacoes = document.querySelectorAll('.centralizacao-item');
|
||||
if (centralizacoes.length > 1) {
|
||||
e.target.closest('.centralizacao-item').remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Salvar comprovante
|
||||
document.getElementById('salvarComprovante').addEventListener('click', function() {
|
||||
const form = document.getElementById('novoComprovanteForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Coletar dados das centralizações
|
||||
const centralizacoes = [];
|
||||
document.querySelectorAll('.centralizacao-item').forEach(item => {
|
||||
centralizacoes.push({
|
||||
tipo_comprovante: item.querySelector('.tipo-comprovante').value,
|
||||
valor: item.querySelector('.valor').value
|
||||
});
|
||||
});
|
||||
|
||||
// Adicionar centralizações ao formData
|
||||
formData.append('centralizacoes', JSON.stringify(centralizacoes));
|
||||
|
||||
fetch('/comprovantes/novo', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || 'Erro ao salvar comprovante');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Erro ao salvar comprovante');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Modal Editar Comprovante -->
|
||||
<div class="modal fade" id="modalEditarComprovante" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Registrar</button>
|
||||
<a href="{{ url_for('listar_assinaturas') }}" class="btn btn-secondary">Voltar</a>
|
||||
<a href="{{ url_for('listar_vendas_jornal') }}" class="btn btn-secondary">Voltar</a>
|
||||
<a href="{{ url_for('home') }}" class="btn btn-outline-primary">Início</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
89
tests/test_routes.py
Normal file
89
tests/test_routes.py
Normal 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
|
||||
Reference in New Issue
Block a user