2025-04-03 15:58:07 -03:00
|
|
|
from datetime import datetime, timedelta
|
2025-03-18 17:31:59 -03:00
|
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
2025-04-02 21:20:48 -03:00
|
|
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text
|
2025-04-03 15:58:07 -03:00
|
|
|
from sqlalchemy.orm import sessionmaker, relationship, backref
|
2025-03-18 17:31:59 -03:00
|
|
|
import os
|
2025-04-03 15:58:07 -03:00
|
|
|
import pyotp
|
2025-03-24 14:50:42 -03:00
|
|
|
from pathlib import Path
|
|
|
|
|
from sqlalchemy.pool import NullPool
|
2025-04-01 15:27:16 -03:00
|
|
|
import secrets
|
|
|
|
|
from flask_mail import Message
|
|
|
|
|
from flask import url_for
|
2025-04-03 15:58:07 -03:00
|
|
|
import enum
|
|
|
|
|
from flask_login import UserMixin
|
|
|
|
|
from .rbac import Role, Permission, role_permissions, user_roles
|
|
|
|
|
from .base import Base, engine, Session
|
2025-04-02 21:20:48 -03:00
|
|
|
import logging
|
2025-03-24 14:50:42 -03:00
|
|
|
|
|
|
|
|
# Configurar caminho do banco de dados
|
|
|
|
|
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
|
|
|
|
db_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
db_path = db_dir / 'database.db'
|
|
|
|
|
|
2025-04-09 09:59:12 -03:00
|
|
|
DATABASE_URL = f"sqlite:///{db_path}"
|
|
|
|
|
engine = create_engine(DATABASE_URL)
|
|
|
|
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
2025-01-08 00:19:49 -03:00
|
|
|
|
2025-02-20 10:39:31 -03:00
|
|
|
def get_db_connection():
|
2025-04-09 09:59:12 -03:00
|
|
|
"""Retorna uma nova sessão do banco de dados"""
|
|
|
|
|
Session = sessionmaker(bind=engine)
|
|
|
|
|
db = Session()
|
|
|
|
|
|
2025-03-24 14:50:42 -03:00
|
|
|
try:
|
2025-04-04 09:24:34 -03:00
|
|
|
# Configurar SQLite para melhor tratamento de concorrência
|
|
|
|
|
db.execute(text("PRAGMA journal_mode=WAL"))
|
|
|
|
|
db.execute(text("PRAGMA busy_timeout=5000"))
|
|
|
|
|
return db
|
|
|
|
|
except:
|
|
|
|
|
db.close()
|
2025-04-03 13:48:09 -03:00
|
|
|
raise
|
2024-11-26 10:57:25 -03:00
|
|
|
|
|
|
|
|
def execute_query(query, params=None):
|
2025-02-20 10:39:31 -03:00
|
|
|
"""
|
|
|
|
|
Executa uma query usando SQLAlchemy
|
|
|
|
|
"""
|
|
|
|
|
session = get_db_connection()
|
2024-11-26 10:57:25 -03:00
|
|
|
try:
|
2025-02-20 10:39:31 -03:00
|
|
|
result = session.execute(query, params)
|
|
|
|
|
session.commit()
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
session.rollback()
|
|
|
|
|
raise e
|
2024-11-26 10:57:25 -03:00
|
|
|
finally:
|
2025-02-20 10:39:31 -03:00
|
|
|
session.close()
|
2025-01-08 00:19:49 -03:00
|
|
|
|
2025-04-03 20:58:02 -03:00
|
|
|
class EstadoMilitante(enum.Enum):
|
|
|
|
|
ATIVO = 'ativo'
|
|
|
|
|
DESLIGADO = 'desligado'
|
|
|
|
|
SUSPENSO = 'suspenso'
|
|
|
|
|
AFASTADO = 'afastado'
|
|
|
|
|
|
2025-04-01 15:27:16 -03:00
|
|
|
class Celula(Base):
|
|
|
|
|
__tablename__ = 'celulas'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
nome = Column(String(100), nullable=False)
|
2025-04-09 10:09:31 -03:00
|
|
|
setor_id = Column(Integer, ForeignKey('setores.id', use_alter=True, name='fk_celula_setor'))
|
|
|
|
|
cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_celula_cr'))
|
|
|
|
|
secretario = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_secretario'))
|
|
|
|
|
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_responsavel_financas'))
|
2025-04-03 10:30:48 -03:00
|
|
|
quadro_orientador = Column(String(255))
|
2025-04-01 15:27:16 -03:00
|
|
|
|
2025-04-03 10:30:48 -03:00
|
|
|
# Relacionamentos
|
2025-04-01 15:27:16 -03:00
|
|
|
setor = relationship("Setor", back_populates="celulas")
|
|
|
|
|
cr = relationship("ComiteRegional", back_populates="celulas")
|
2025-04-03 10:30:48 -03:00
|
|
|
militantes = relationship("Militante", back_populates="celula", foreign_keys="[Militante.celula_id]")
|
|
|
|
|
secretario_rel = relationship("Militante", foreign_keys=[secretario])
|
|
|
|
|
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
|
|
|
|
|
pagamentos = relationship("PagamentoCelula", back_populates="celula")
|
2025-04-03 15:58:07 -03:00
|
|
|
usuarios = relationship("Usuario", back_populates="celula")
|
2025-04-01 15:27:16 -03:00
|
|
|
|
|
|
|
|
class ComiteRegional(Base):
|
|
|
|
|
__tablename__ = 'comites_regionais'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
nome = Column(String(100), nullable=False)
|
2025-04-09 10:09:31 -03:00
|
|
|
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_financas'))
|
|
|
|
|
responsavel_formacao = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_responsavel_formacao'))
|
|
|
|
|
secretario_organizacao = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_secretario_organizacao'))
|
|
|
|
|
correspondente_jornal = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_cr_correspondente_jornal'))
|
2025-04-01 15:27:16 -03:00
|
|
|
|
2025-04-03 10:30:48 -03:00
|
|
|
# Relacionamentos
|
|
|
|
|
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
|
|
|
|
|
responsavel_formacao_rel = relationship("Militante", foreign_keys=[responsavel_formacao])
|
|
|
|
|
secretario_organizacao_rel = relationship("Militante", foreign_keys=[secretario_organizacao])
|
|
|
|
|
correspondente_jornal_rel = relationship("Militante", foreign_keys=[correspondente_jornal])
|
2025-04-01 15:27:16 -03:00
|
|
|
setores = relationship("Setor", back_populates="cr")
|
|
|
|
|
celulas = relationship("Celula", back_populates="cr")
|
2025-04-03 15:58:07 -03:00
|
|
|
usuarios = relationship("Usuario", back_populates="cr")
|
2025-04-01 15:27:16 -03:00
|
|
|
|
2025-04-03 10:30:48 -03:00
|
|
|
class EmailMilitante(Base):
|
|
|
|
|
__tablename__ = 'emails_militantes'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
endereco_email = Column(String(100))
|
|
|
|
|
militante = relationship("Militante", back_populates="emails")
|
|
|
|
|
|
|
|
|
|
class Endereco(Base):
|
|
|
|
|
__tablename__ = 'enderecos'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
estado = Column(String(2))
|
|
|
|
|
cidade = Column(String(50))
|
|
|
|
|
bairro = Column(String(50))
|
|
|
|
|
rua = Column(String(100))
|
|
|
|
|
numero = Column(String(10))
|
|
|
|
|
complemento = Column(String(50))
|
|
|
|
|
cep = Column(String(9))
|
|
|
|
|
militantes = relationship("Militante", back_populates="endereco")
|
|
|
|
|
|
|
|
|
|
class RedeSocial(Base):
|
|
|
|
|
__tablename__ = 'redes_sociais'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
tipo = Column(String(20)) # Instagram, TikTok, Discord, etc.
|
|
|
|
|
identificador = Column(String(100))
|
|
|
|
|
militante = relationship("Militante", back_populates="redes_sociais")
|
|
|
|
|
|
2025-01-08 00:19:49 -03:00
|
|
|
class Militante(Base):
|
|
|
|
|
__tablename__ = 'militantes'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
nome = Column(String(100), nullable=False)
|
|
|
|
|
cpf = Column(String(14), unique=True)
|
2025-04-03 10:30:48 -03:00
|
|
|
# Novos campos básicos
|
|
|
|
|
titulo_eleitoral = Column(String(20))
|
|
|
|
|
data_nascimento = Column(Date)
|
|
|
|
|
data_entrada_oci = Column(Date)
|
|
|
|
|
data_efetivacao_oci = Column(Date)
|
|
|
|
|
# Campos de contato
|
|
|
|
|
telefone1 = Column(String(15))
|
|
|
|
|
telefone2 = Column(String(15))
|
|
|
|
|
# Relacionamento para múltiplos emails
|
|
|
|
|
emails = relationship("EmailMilitante", back_populates="militante")
|
|
|
|
|
# Endereço
|
2025-04-09 10:09:31 -03:00
|
|
|
endereco_id = Column(Integer, ForeignKey('enderecos.id', use_alter=True, name='fk_militante_endereco'))
|
2025-04-03 10:30:48 -03:00
|
|
|
endereco = relationship("Endereco", back_populates="militantes")
|
|
|
|
|
# Redes sociais
|
|
|
|
|
redes_sociais = relationship("RedeSocial", back_populates="militante")
|
|
|
|
|
# Campos profissionais
|
|
|
|
|
profissao = Column(String(100))
|
|
|
|
|
regime_trabalho = Column(String(50)) # CLT, Estatutário, etc.
|
|
|
|
|
empresa = Column(String(100))
|
|
|
|
|
contratante = Column(String(100)) # Para terceirizados
|
|
|
|
|
# Campos acadêmicos
|
|
|
|
|
instituicao_ensino = Column(String(100))
|
|
|
|
|
tipo_instituicao = Column(String(20)) # Federal, Estadual, etc.
|
|
|
|
|
# Campos sindicais
|
|
|
|
|
sindicato = Column(String(100))
|
|
|
|
|
cargo_sindical = Column(String(50))
|
|
|
|
|
dirigente_sindical = Column(Boolean)
|
|
|
|
|
central_sindical = Column(String(100))
|
|
|
|
|
# Responsável pelo cadastro
|
2025-04-09 10:09:31 -03:00
|
|
|
registrado_por = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_militante_registrado_por'))
|
2025-04-03 10:30:48 -03:00
|
|
|
# Campos existentes
|
2025-04-09 10:09:31 -03:00
|
|
|
celula_id = Column(Integer, ForeignKey('celulas.id', use_alter=True, name='fk_militante_celula'))
|
2025-04-03 10:30:48 -03:00
|
|
|
responsabilidades = Column(Integer, default=0)
|
2025-04-01 15:27:16 -03:00
|
|
|
otp_secret = Column(String(32))
|
|
|
|
|
temp_token = Column(String(64))
|
|
|
|
|
temp_token_expiry = Column(DateTime)
|
2025-04-03 15:58:07 -03:00
|
|
|
# Novo campo para Quadro-Orientador
|
|
|
|
|
quadro_orientador = Column(Boolean, default=False)
|
|
|
|
|
# Campos para Aspirante
|
|
|
|
|
aspirante = Column(Boolean, default=True) # Por padrão, todo novo militante é aspirante
|
|
|
|
|
data_inicio_aspirante = Column(DateTime, default=datetime.utcnow)
|
|
|
|
|
avaliacao_aspirante = Column(Text)
|
|
|
|
|
data_avaliacao_aspirante = Column(DateTime)
|
2025-04-03 20:58:02 -03:00
|
|
|
|
|
|
|
|
# Campos para estado do militante
|
|
|
|
|
estado = Column(Enum(EstadoMilitante), default=EstadoMilitante.ATIVO)
|
|
|
|
|
data_desligamento = Column(DateTime)
|
|
|
|
|
motivo_desligamento = Column(Text)
|
2025-01-08 00:19:49 -03:00
|
|
|
|
2025-04-03 10:30:48 -03:00
|
|
|
# Relacionamentos existentes
|
2025-01-08 00:19:49 -03:00
|
|
|
cotas_mensais = relationship("CotaMensal", back_populates="militante")
|
|
|
|
|
pagamentos = relationship("Pagamento", back_populates="militante")
|
|
|
|
|
materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
|
|
|
|
|
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
|
|
|
|
|
assinaturas = relationship("AssinaturaAnual", back_populates="militante")
|
2025-04-03 10:30:48 -03:00
|
|
|
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
|
2025-04-01 15:27:16 -03:00
|
|
|
|
|
|
|
|
# Constantes para responsabilidades
|
|
|
|
|
SECRETARIO = 1
|
|
|
|
|
TESOUREIRO = 2
|
|
|
|
|
IMPRENSA = 4
|
|
|
|
|
MNS = 8
|
|
|
|
|
MPS = 16
|
|
|
|
|
JUVENTUDE = 32
|
2025-04-03 15:58:07 -03:00
|
|
|
QUADRO_ORIENTADOR = 64
|
|
|
|
|
ASPIRANTE = 128
|
|
|
|
|
RESPONSAVEL_FINANCAS = 256
|
|
|
|
|
RESPONSAVEL_IMPRENSA = 512
|
2025-04-01 15:27:16 -03:00
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_responsabilidades_list():
|
|
|
|
|
return [
|
|
|
|
|
(Militante.SECRETARIO, "Secretário"),
|
|
|
|
|
(Militante.TESOUREIRO, "Tesoureiro"),
|
|
|
|
|
(Militante.IMPRENSA, "Imprensa"),
|
|
|
|
|
(Militante.MNS, "MNS"),
|
|
|
|
|
(Militante.MPS, "MPS"),
|
2025-04-03 15:58:07 -03:00
|
|
|
(Militante.JUVENTUDE, "Juventude"),
|
|
|
|
|
(Militante.QUADRO_ORIENTADOR, "Quadro-Orientador"),
|
|
|
|
|
(Militante.ASPIRANTE, "Aspirante"),
|
|
|
|
|
(Militante.RESPONSAVEL_FINANCAS, "Responsável de Finanças"),
|
|
|
|
|
(Militante.RESPONSAVEL_IMPRENSA, "Responsável de Imprensa")
|
2025-04-01 15:27:16 -03:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def set_responsabilidades(self, resp_list):
|
|
|
|
|
"""
|
|
|
|
|
Define as responsabilidades do militante
|
|
|
|
|
resp_list: lista de inteiros representando as responsabilidades
|
|
|
|
|
"""
|
|
|
|
|
self.responsabilidades = sum(resp_list)
|
|
|
|
|
|
|
|
|
|
def get_responsabilidades(self):
|
|
|
|
|
"""
|
|
|
|
|
Retorna lista de responsabilidades ativas
|
|
|
|
|
"""
|
|
|
|
|
resp = []
|
|
|
|
|
for valor, nome in self.get_responsabilidades_list():
|
|
|
|
|
if self.responsabilidades & valor:
|
|
|
|
|
resp.append(nome)
|
|
|
|
|
return resp
|
|
|
|
|
|
|
|
|
|
def generate_temp_token(self):
|
|
|
|
|
"""
|
|
|
|
|
Gera um token temporário para acesso ao QR code
|
|
|
|
|
"""
|
|
|
|
|
self.temp_token = secrets.token_urlsafe(32)
|
|
|
|
|
self.temp_token_expiry = datetime.now() + timedelta(hours=48)
|
|
|
|
|
return self.temp_token
|
|
|
|
|
|
|
|
|
|
def send_otp_email(self, mail):
|
|
|
|
|
"""
|
|
|
|
|
Envia email com link para QR code
|
|
|
|
|
"""
|
|
|
|
|
token = self.generate_temp_token()
|
|
|
|
|
qr_url = url_for('get_qr_code', token=token, _external=True)
|
|
|
|
|
|
|
|
|
|
msg = Message(
|
|
|
|
|
'Configuração de Autenticação em Duas Etapas',
|
|
|
|
|
recipients=[self.email]
|
|
|
|
|
)
|
|
|
|
|
msg.body = f"""
|
|
|
|
|
Olá {self.nome},
|
|
|
|
|
|
|
|
|
|
Para configurar sua autenticação em duas etapas, acesse o link abaixo:
|
|
|
|
|
{qr_url}
|
|
|
|
|
|
|
|
|
|
Este link expirará em 48 horas.
|
|
|
|
|
|
|
|
|
|
Instruções:
|
|
|
|
|
1. Instale um aplicativo autenticador (Google Authenticator, Microsoft Authenticator)
|
|
|
|
|
2. Acesse o link acima
|
|
|
|
|
3. Escaneie o QR code com o aplicativo
|
|
|
|
|
4. Use o código gerado para fazer login no sistema
|
|
|
|
|
|
|
|
|
|
Atenciosamente,
|
|
|
|
|
Sistema de Controles
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
mail.send(msg)
|
2025-01-08 00:19:49 -03:00
|
|
|
|
2025-04-03 15:58:07 -03:00
|
|
|
def generate_username(self):
|
|
|
|
|
"""Gera um nome de usuário único baseado no primeiro nome e um código"""
|
|
|
|
|
from sqlalchemy import func
|
|
|
|
|
db = get_db_connection()
|
|
|
|
|
try:
|
|
|
|
|
# Pega o primeiro nome
|
|
|
|
|
primeiro_nome = self.nome.split()[0].lower()
|
|
|
|
|
|
|
|
|
|
# Conta quantos usuários já existem com esse prefixo
|
|
|
|
|
count = db.query(func.count(Usuario.id)).filter(
|
|
|
|
|
Usuario.username.like(f"{primeiro_nome}%")
|
|
|
|
|
).scalar()
|
|
|
|
|
|
|
|
|
|
# Gera o código (número sequencial)
|
|
|
|
|
codigo = str(count + 1).zfill(3)
|
|
|
|
|
|
|
|
|
|
return f"{primeiro_nome}{codigo}"
|
|
|
|
|
finally:
|
|
|
|
|
db.close()
|
|
|
|
|
|
2025-01-08 00:19:49 -03:00
|
|
|
class CotaMensal(Base):
|
|
|
|
|
__tablename__ = 'cotas_mensais'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
valor_antigo = Column(Numeric(10, 2), nullable=False)
|
|
|
|
|
valor_novo = Column(Numeric(10, 2), nullable=False)
|
|
|
|
|
data_alteracao = Column(Date, nullable=False)
|
2025-04-02 21:20:48 -03:00
|
|
|
data_vencimento = Column(Date, nullable=False)
|
|
|
|
|
pago = Column(Boolean, default=False)
|
2025-01-08 00:19:49 -03:00
|
|
|
|
|
|
|
|
militante = relationship("Militante", back_populates="cotas_mensais")
|
|
|
|
|
|
|
|
|
|
class TipoPagamento(Base):
|
|
|
|
|
__tablename__ = 'tipos_pagamento'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
descricao = Column(String(100), nullable=False)
|
|
|
|
|
|
|
|
|
|
class Pagamento(Base):
|
|
|
|
|
__tablename__ = 'pagamentos'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
2025-04-03 10:30:48 -03:00
|
|
|
tipo_pagamento = Column(String(50)) # Cota, Jornal, Assinatura, etc.
|
|
|
|
|
mes_referencia = Column(Date)
|
|
|
|
|
numero_jornal = Column(String(20))
|
|
|
|
|
numero_inicial_assinatura = Column(String(20))
|
|
|
|
|
numero_final_assinatura = Column(String(20))
|
|
|
|
|
campanha_financeira = Column(String(50))
|
2025-01-08 00:19:49 -03:00
|
|
|
valor = Column(Numeric(10, 2), nullable=False)
|
|
|
|
|
data_pagamento = Column(Date, nullable=False)
|
|
|
|
|
|
|
|
|
|
militante = relationship("Militante", back_populates="pagamentos")
|
2025-04-03 10:30:48 -03:00
|
|
|
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")
|
2025-01-08 00:19:49 -03:00
|
|
|
|
|
|
|
|
class TipoMaterial(Base):
|
|
|
|
|
__tablename__ = 'tipos_materiais'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
descricao = Column(String(100), nullable=False)
|
|
|
|
|
|
|
|
|
|
materiais_vendidos = relationship("MaterialVendido", back_populates="tipo_material")
|
|
|
|
|
assinaturas = relationship("AssinaturaAnual", back_populates="tipo_material")
|
|
|
|
|
|
|
|
|
|
class MaterialVendido(Base):
|
|
|
|
|
__tablename__ = 'materiais_vendidos'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
|
|
|
|
|
descricao = Column(String(255), nullable=False)
|
|
|
|
|
valor = Column(Numeric(10, 2), nullable=False)
|
|
|
|
|
data_venda = Column(Date, nullable=False)
|
|
|
|
|
|
|
|
|
|
militante = relationship("Militante", back_populates="materiais_vendidos")
|
|
|
|
|
tipo_material = relationship("TipoMaterial", back_populates="materiais_vendidos")
|
|
|
|
|
|
|
|
|
|
class VendaJornalAvulso(Base):
|
|
|
|
|
__tablename__ = 'vendas_jornais_avulsos'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
quantidade = Column(Integer, nullable=False)
|
|
|
|
|
valor_total = Column(Numeric(10, 2), nullable=False)
|
|
|
|
|
data_venda = Column(Date, nullable=False)
|
|
|
|
|
|
|
|
|
|
militante = relationship("Militante", back_populates="vendas_jornais")
|
|
|
|
|
|
|
|
|
|
class AssinaturaAnual(Base):
|
|
|
|
|
__tablename__ = 'assinaturas_anuais'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
|
|
|
|
|
quantidade = Column(Integer, nullable=False)
|
|
|
|
|
valor_total = Column(Numeric(10, 2), nullable=False)
|
|
|
|
|
data_inicio = Column(Date, nullable=False)
|
|
|
|
|
data_fim = Column(Date, nullable=False)
|
|
|
|
|
|
|
|
|
|
militante = relationship("Militante", back_populates="assinaturas")
|
|
|
|
|
tipo_material = relationship("TipoMaterial", back_populates="assinaturas")
|
|
|
|
|
|
|
|
|
|
class Setor(Base):
|
|
|
|
|
__tablename__ = 'setores'
|
|
|
|
|
|
2025-04-03 10:30:48 -03:00
|
|
|
id = Column(Integer, primary_key=True)
|
2025-01-08 00:19:49 -03:00
|
|
|
nome = Column(String(100), nullable=False)
|
2025-04-09 10:09:31 -03:00
|
|
|
cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_setor_cr'))
|
|
|
|
|
responsavel = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_setor_responsavel'))
|
|
|
|
|
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_setor_responsavel_financas'))
|
2025-04-03 10:30:48 -03:00
|
|
|
|
|
|
|
|
# Relacionamentos
|
|
|
|
|
cr = relationship("ComiteRegional", back_populates="setores")
|
|
|
|
|
responsavel_rel = relationship("Militante", foreign_keys=[responsavel])
|
|
|
|
|
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
|
2025-03-18 17:31:59 -03:00
|
|
|
usuarios = relationship("Usuario", back_populates="setor")
|
2025-04-01 15:27:16 -03:00
|
|
|
celulas = relationship("Celula", back_populates="setor")
|
2025-04-03 10:30:48 -03:00
|
|
|
relatorios_cotas = relationship("RelatorioCotasMensais", back_populates="setor")
|
|
|
|
|
relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="setor")
|
2025-01-08 00:19:49 -03:00
|
|
|
|
|
|
|
|
class ComiteCentral(Base):
|
|
|
|
|
__tablename__ = 'comites_centrais'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
nome = Column(String(100), nullable=False)
|
|
|
|
|
|
|
|
|
|
relatorios_cotas = relationship("RelatorioCotasMensais", back_populates="comite")
|
|
|
|
|
relatorios_vendas = relationship("RelatorioVendasMateriais", back_populates="comite")
|
|
|
|
|
|
|
|
|
|
class RelatorioCotasMensais(Base):
|
|
|
|
|
__tablename__ = 'relatorio_cotas_mensais'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
setor_id = Column(Integer, ForeignKey('setores.id'))
|
|
|
|
|
comite_id = Column(Integer, ForeignKey('comites_centrais.id'))
|
|
|
|
|
total_cotas = Column(Numeric(10, 2), nullable=False)
|
|
|
|
|
data_relatorio = Column(Date, nullable=False)
|
|
|
|
|
|
|
|
|
|
setor = relationship("Setor", back_populates="relatorios_cotas")
|
|
|
|
|
comite = relationship("ComiteCentral", back_populates="relatorios_cotas")
|
|
|
|
|
|
|
|
|
|
class RelatorioVendasMateriais(Base):
|
|
|
|
|
__tablename__ = 'relatorio_vendas_materiais'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
setor_id = Column(Integer, ForeignKey('setores.id'))
|
|
|
|
|
comite_id = Column(Integer, ForeignKey('comites_centrais.id'))
|
|
|
|
|
total_vendas = Column(Numeric(10, 2), nullable=False)
|
|
|
|
|
data_relatorio = Column(Date, nullable=False)
|
|
|
|
|
|
|
|
|
|
setor = relationship("Setor", back_populates="relatorios_vendas")
|
|
|
|
|
comite = relationship("ComiteCentral", back_populates="relatorios_vendas")
|
|
|
|
|
|
2025-04-03 15:58:07 -03:00
|
|
|
class TipoUsuario(enum.Enum):
|
|
|
|
|
ADMIN = "admin"
|
|
|
|
|
CR_RESPONSAVEL = "cr_responsavel"
|
|
|
|
|
SETOR_RESPONSAVEL = "setor_responsavel"
|
|
|
|
|
USUARIO = "usuario"
|
|
|
|
|
|
|
|
|
|
class Usuario(Base, UserMixin):
|
2025-03-18 17:31:59 -03:00
|
|
|
__tablename__ = 'usuarios'
|
|
|
|
|
|
2025-04-03 15:58:07 -03:00
|
|
|
id = Column(Integer, primary_key=True)
|
2025-03-18 17:31:59 -03:00
|
|
|
username = Column(String(50), unique=True, nullable=False)
|
|
|
|
|
password_hash = Column(String(255), nullable=False)
|
2025-03-24 16:34:38 -03:00
|
|
|
email = Column(String(100), unique=True, nullable=False)
|
|
|
|
|
otp_secret = Column(String(32))
|
|
|
|
|
role_id = Column(Integer, ForeignKey('roles.id'))
|
|
|
|
|
setor_id = Column(Integer, ForeignKey('setores.id'))
|
2025-03-18 17:31:59 -03:00
|
|
|
ativo = Column(Boolean, default=True)
|
|
|
|
|
is_admin = Column(Boolean, default=False)
|
2025-03-24 16:34:38 -03:00
|
|
|
ultimo_login = Column(DateTime)
|
|
|
|
|
ultimo_logout = Column(DateTime)
|
|
|
|
|
motivo_logout = Column(String(100))
|
2025-04-03 10:30:48 -03:00
|
|
|
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
|
|
|
|
|
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
2025-04-03 15:58:07 -03:00
|
|
|
session_timeout = Column(Integer, default=30)
|
|
|
|
|
tipo = Column(String(17), nullable=False)
|
|
|
|
|
ultima_atividade = Column(DateTime, default=datetime.utcnow)
|
|
|
|
|
# Relacionamento com militante
|
|
|
|
|
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
militante = relationship("Militante", backref=backref("usuario", uselist=False))
|
2025-03-24 16:34:38 -03:00
|
|
|
|
2025-04-03 15:58:07 -03:00
|
|
|
# Relacionamentos
|
|
|
|
|
roles = relationship("Role", secondary="user_roles", back_populates="users")
|
|
|
|
|
setor = relationship('Setor', back_populates='usuarios')
|
|
|
|
|
cr = relationship('ComiteRegional', back_populates='usuarios')
|
|
|
|
|
celula = relationship('Celula', back_populates='usuarios')
|
2025-03-18 17:31:59 -03:00
|
|
|
|
2025-04-02 21:20:48 -03:00
|
|
|
def __init__(self, username, email=None, is_admin=False):
|
2025-03-18 17:31:59 -03:00
|
|
|
self.username = username
|
2025-04-02 21:20:48 -03:00
|
|
|
self.email = email
|
2025-03-18 17:31:59 -03:00
|
|
|
self.is_admin = is_admin
|
2025-04-03 15:58:07 -03:00
|
|
|
self.email = email
|
2025-03-18 17:31:59 -03:00
|
|
|
self.ativo = True
|
2025-04-03 15:58:07 -03:00
|
|
|
self.session_timeout = 30
|
2025-04-02 21:20:48 -03:00
|
|
|
self.tipo = "USUARIO"
|
2025-04-03 15:58:07 -03:00
|
|
|
self.ultima_atividade = datetime.utcnow()
|
2025-03-18 17:31:59 -03:00
|
|
|
|
2025-04-02 21:20:48 -03:00
|
|
|
def set_password(self, password):
|
|
|
|
|
self.password_hash = generate_password_hash(password)
|
|
|
|
|
|
2025-03-18 17:31:59 -03:00
|
|
|
def check_password(self, password):
|
2025-03-24 16:34:38 -03:00
|
|
|
return check_password_hash(self.password_hash, password)
|
2025-03-18 17:31:59 -03:00
|
|
|
|
2025-04-03 15:58:07 -03:00
|
|
|
def update_last_activity(self):
|
|
|
|
|
self.ultima_atividade = datetime.utcnow()
|
|
|
|
|
|
|
|
|
|
def is_session_expired(self):
|
|
|
|
|
if not self.ultima_atividade:
|
|
|
|
|
return True
|
|
|
|
|
time_diff = datetime.utcnow() - self.ultima_atividade
|
|
|
|
|
return time_diff.total_seconds() > (self.session_timeout * 60)
|
|
|
|
|
|
|
|
|
|
def check_session_timeout(self):
|
|
|
|
|
"""Verifica se a sessão do usuário expirou"""
|
|
|
|
|
if not self.ultima_atividade:
|
|
|
|
|
return True
|
|
|
|
|
time_diff = datetime.utcnow() - self.ultima_atividade
|
|
|
|
|
return time_diff.total_seconds() > (self.session_timeout * 60)
|
|
|
|
|
|
|
|
|
|
def has_permission(self, permission_name):
|
2025-04-04 02:34:51 -03:00
|
|
|
"""Verifica se o usuário tem uma permissão específica"""
|
|
|
|
|
if self.is_admin: # Se for admin, tem todas as permissões
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
# Verifica se o usuário tem a permissão através de suas roles
|
2025-04-03 15:58:07 -03:00
|
|
|
for role in self.roles:
|
|
|
|
|
for permission in role.permissions:
|
|
|
|
|
if permission.nome == permission_name:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def has_role(self, role_nivel):
|
|
|
|
|
"""Verifica se o usuário tem um determinado nível de role"""
|
|
|
|
|
for role in self.roles:
|
|
|
|
|
if role.nivel == role_nivel:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
2025-03-18 17:31:59 -03:00
|
|
|
|
|
|
|
|
def get_otp_uri(self):
|
2025-04-03 15:58:07 -03:00
|
|
|
"""Gera a URI para autenticação em duas etapas"""
|
2025-03-24 16:34:38 -03:00
|
|
|
if not self.otp_secret:
|
|
|
|
|
self.otp_secret = pyotp.random_base32()
|
2025-04-03 15:58:07 -03:00
|
|
|
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
|
|
|
|
|
self.username,
|
2025-03-27 14:34:16 -03:00
|
|
|
issuer_name="Sistema de Controles"
|
2025-03-24 16:34:38 -03:00
|
|
|
)
|
2025-03-18 17:31:59 -03:00
|
|
|
|
2025-04-03 15:58:07 -03:00
|
|
|
def verify_otp(self, code):
|
|
|
|
|
"""Verifica se um código OTP é válido"""
|
|
|
|
|
if not self.otp_secret:
|
|
|
|
|
print(f"Erro: OTP secret não configurado para o usuário {self.username}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
print(f"Verificando OTP para usuário {self.username}")
|
|
|
|
|
print(f"OTP Secret: {self.otp_secret}")
|
|
|
|
|
print(f"Código fornecido: {code}")
|
|
|
|
|
|
|
|
|
|
totp = pyotp.totp.TOTP(self.otp_secret)
|
|
|
|
|
is_valid = totp.verify(code)
|
|
|
|
|
|
|
|
|
|
print(f"Resultado da verificação: {'Válido' if is_valid else 'Inválido'}")
|
|
|
|
|
print(f"Tempo atual: {datetime.utcnow()}")
|
|
|
|
|
print(f"Período atual: {totp.timecode(datetime.utcnow())}")
|
|
|
|
|
|
|
|
|
|
return is_valid
|
2025-03-18 17:31:59 -03:00
|
|
|
|
2025-04-03 15:58:07 -03:00
|
|
|
def logout(self):
|
|
|
|
|
"""Registra o logout do usuário"""
|
|
|
|
|
self.ultimo_logout = datetime.utcnow()
|
|
|
|
|
self.motivo_logout = "Logout manual"
|
|
|
|
|
self.ultima_atividade = None
|
2025-03-18 17:31:59 -03:00
|
|
|
|
2025-04-03 10:30:48 -03:00
|
|
|
class PagamentoCelula(Base):
|
|
|
|
|
__tablename__ = 'pagamentos_celula'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
|
|
|
|
data = Column(Date)
|
|
|
|
|
valor = Column(Numeric(10, 2))
|
|
|
|
|
metodo_pagamento = Column(String(20)) # PIX, Dinheiro, etc.
|
|
|
|
|
codigo_pix = Column(String(100))
|
|
|
|
|
descricao = Column(String(255))
|
|
|
|
|
registrado_por = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
|
|
|
|
|
celula = relationship("Celula", back_populates="pagamentos")
|
|
|
|
|
registrado_por_rel = relationship("Militante", foreign_keys=[registrado_por])
|
|
|
|
|
|
|
|
|
|
class Atividade(Base):
|
|
|
|
|
__tablename__ = 'atividades'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
descricao = Column(String(255))
|
|
|
|
|
data = Column(Date)
|
|
|
|
|
responsavel1 = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
responsavel2 = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
|
|
|
|
|
responsavel1_rel = relationship("Militante", foreign_keys=[responsavel1])
|
|
|
|
|
responsavel2_rel = relationship("Militante", foreign_keys=[responsavel2])
|
|
|
|
|
materiais = relationship("MaterialAtividade", back_populates="atividade")
|
|
|
|
|
|
|
|
|
|
class MaterialAtividade(Base):
|
|
|
|
|
__tablename__ = 'materiais_atividades'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
atividade_id = Column(Integer, ForeignKey('atividades.id'))
|
|
|
|
|
tipo = Column(String(20)) # Jornal, Revista, etc.
|
|
|
|
|
quantidade = Column(Integer)
|
|
|
|
|
detalhes = Column(String(255))
|
|
|
|
|
|
|
|
|
|
atividade = relationship("Atividade", back_populates="materiais")
|
|
|
|
|
|
|
|
|
|
class Relatorio(Base):
|
|
|
|
|
__tablename__ = 'relatorios'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
tipo = Column(String(50)) # Semanal, Quinzenal, Mensal
|
|
|
|
|
periodo_inicio = Column(Date)
|
|
|
|
|
periodo_fim = Column(Date)
|
|
|
|
|
gerado_por = Column(Integer, ForeignKey('militantes.id'))
|
|
|
|
|
conteudo = Column(Text)
|
|
|
|
|
# Relacionamento hierárquico
|
|
|
|
|
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
|
|
|
|
setor_id = Column(Integer, ForeignKey('setores.id'))
|
|
|
|
|
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
|
|
|
|
|
|
|
|
|
|
gerado_por_rel = relationship("Militante", foreign_keys=[gerado_por])
|
|
|
|
|
celula = relationship("Celula", foreign_keys=[celula_id])
|
|
|
|
|
setor = relationship("Setor", foreign_keys=[setor_id])
|
|
|
|
|
cr = relationship("ComiteRegional", foreign_keys=[cr_id])
|
|
|
|
|
|
|
|
|
|
class TransacaoPIX(Base):
|
|
|
|
|
__tablename__ = 'transacoes_pix'
|
|
|
|
|
|
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
|
|
|
chave_pix = Column(String(100))
|
|
|
|
|
valor = Column(Numeric(10, 2))
|
|
|
|
|
data_geracao = Column(DateTime)
|
|
|
|
|
data_pagamento = Column(DateTime)
|
|
|
|
|
status = Column(String(20)) # Pendente, Pago, Expirado
|
|
|
|
|
qr_code = Column(Text)
|
|
|
|
|
pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
|
|
|
|
|
|
|
|
|
|
pagamento = relationship("Pagamento", back_populates="transacoes_pix")
|
|
|
|
|
|
2025-04-03 15:58:07 -03:00
|
|
|
def init_database():
|
|
|
|
|
"""Inicializa o banco de dados com dados básicos"""
|
|
|
|
|
print("Inicializando banco de dados...")
|
|
|
|
|
|
2025-04-04 09:24:34 -03:00
|
|
|
session = get_db_connection()
|
2025-04-03 15:58:07 -03:00
|
|
|
try:
|
2025-04-04 09:24:34 -03:00
|
|
|
# Criar todas as tabelas
|
|
|
|
|
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
|
|
|
|
|
Base.metadata.create_all(engine)
|
|
|
|
|
|
|
|
|
|
# Criar roles padrão
|
|
|
|
|
roles = [
|
|
|
|
|
("Administrador", Role.SECRETARIO_GERAL),
|
|
|
|
|
("Secretário", Role.SECRETARIO_CELULA),
|
|
|
|
|
("Militante", Role.MILITANTE_BASICO)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for nome, nivel in roles:
|
|
|
|
|
if not session.query(Role).filter_by(nome=nome).first():
|
|
|
|
|
role = Role(nome=nome, nivel=nivel)
|
|
|
|
|
session.add(role)
|
|
|
|
|
session.commit()
|
|
|
|
|
|
|
|
|
|
# Criar setores padrão
|
|
|
|
|
setores = ["Setor 1", "Setor 2", "Setor 3"]
|
|
|
|
|
for nome in setores:
|
|
|
|
|
if not session.query(Setor).filter_by(nome=nome).first():
|
|
|
|
|
setor = Setor(nome=nome)
|
|
|
|
|
session.add(setor)
|
|
|
|
|
session.commit()
|
|
|
|
|
|
|
|
|
|
# Criar comitês padrão
|
|
|
|
|
comites = ["Comitê 1", "Comitê 2", "Comitê 3"]
|
|
|
|
|
for nome in comites:
|
|
|
|
|
if not session.query(ComiteCentral).filter_by(nome=nome).first():
|
|
|
|
|
comite = ComiteCentral(nome=nome)
|
|
|
|
|
session.add(comite)
|
2025-04-03 15:58:07 -03:00
|
|
|
session.commit()
|
|
|
|
|
|
2025-04-09 09:59:12 -03:00
|
|
|
# Gerar OTP para admin
|
|
|
|
|
admin_otp_secret = pyotp.random_base32()
|
|
|
|
|
print(f"Novo OTP gerado: {admin_otp_secret}")
|
2025-04-03 15:58:07 -03:00
|
|
|
|
|
|
|
|
# Criar usuário admin
|
2025-04-04 09:24:34 -03:00
|
|
|
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
|
|
|
|
setor = session.query(Setor).first()
|
|
|
|
|
|
2025-04-03 15:58:07 -03:00
|
|
|
admin = Usuario(
|
|
|
|
|
username="admin",
|
|
|
|
|
email="admin@example.com",
|
2025-04-03 20:58:02 -03:00
|
|
|
is_admin=True
|
2025-04-03 15:58:07 -03:00
|
|
|
)
|
2025-04-03 20:58:02 -03:00
|
|
|
admin.set_password("admin123")
|
|
|
|
|
admin.tipo = "ADMIN"
|
2025-04-03 15:58:07 -03:00
|
|
|
admin.otp_secret = admin_otp_secret
|
2025-04-04 09:24:34 -03:00
|
|
|
admin.roles.append(admin_role)
|
|
|
|
|
admin.setor = setor
|
2025-04-03 15:58:07 -03:00
|
|
|
session.add(admin)
|
|
|
|
|
session.commit()
|
|
|
|
|
|
2025-04-09 09:59:12 -03:00
|
|
|
# Gerar QR code
|
|
|
|
|
totp = pyotp.totp.TOTP(admin_otp_secret)
|
|
|
|
|
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
|
|
|
|
|
|
|
|
|
|
import qrcode
|
|
|
|
|
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
|
|
|
|
qr.add_data(provisioning_uri)
|
|
|
|
|
qr.make(fit=True)
|
|
|
|
|
img = qr.make_image(fill_color="black", back_color="white")
|
|
|
|
|
img.save('admin_qr.png')
|
2025-04-03 15:58:07 -03:00
|
|
|
|
|
|
|
|
print("=== Usuário Admin Criado ===")
|
|
|
|
|
print(f"Username: admin")
|
|
|
|
|
print(f"Senha: admin123")
|
|
|
|
|
print(f"Email: {admin.email}")
|
2025-04-09 09:59:12 -03:00
|
|
|
print(f"OTP Secret: {admin_otp_secret}")
|
|
|
|
|
print(f"QR Code: admin_qr.png")
|
2025-04-03 15:58:07 -03:00
|
|
|
|
2025-04-04 09:24:34 -03:00
|
|
|
# Importar e executar o seed após criar todas as dependências
|
|
|
|
|
from seed_data import seed_database
|
|
|
|
|
print("\nPopulando banco de dados com dados de teste...")
|
|
|
|
|
seed_database()
|
|
|
|
|
print("Dados de teste criados com sucesso!")
|
|
|
|
|
|
2025-03-18 17:31:59 -03:00
|
|
|
except Exception as e:
|
2025-03-24 14:50:42 -03:00
|
|
|
print(f"Erro na inicialização do banco: {e}")
|
2025-03-18 17:31:59 -03:00
|
|
|
session.rollback()
|
2025-03-24 14:50:42 -03:00
|
|
|
raise
|
2025-03-18 17:31:59 -03:00
|
|
|
finally:
|
|
|
|
|
session.close()
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2025-03-24 14:50:42 -03:00
|
|
|
init_database()
|