Introdução
Neste primeiro projeto com Kivy, vamos criar um aplicativo simples para entender como uma aplicação pode ser organizada na prática. A ideia é construir uma estrutura parecida com a de muitos aplicativos reais. Teremos um rodapé fixo com quatro botões principais, que ficarão sempre visíveis na parte inferior da tela. Cada botão será responsável por abrir uma tela diferente do aplicativo.
Essas quatro telas serão as telas principais da aplicação. Elas funcionarão como as áreas centrais do app, permitindo que o usuário navegue entre diferentes partes do sistema sem sair da estrutura principal. Além dessas telas principais, também teremos telas específicas para determinadas funções, como cadastrar informações, adicionar gastos, registrar dados e executar outras ações relacionadas ao funcionamento do aplicativo.
Com isso, o objetivo deste primeiro app não é apenas mostrar componentes isolados do Kivy, mas criar uma base mais próxima de um aplicativo real, com navegação, organização visual e separação entre telas principais e telas de ação.
Para começar, crie os arquivos abaixo com o conteúdo deles:
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import Screen
from db import BancoDados
class LayoutPrincipal(BoxLayout):
pass
class TelaResumo(Screen):
pass
class TelaRenda(Screen):
pass
class TelaGastos(Screen):
pass
class TelaAssinaturas(Screen):
pass
class TelaParcelas(Screen):
pass
class FinancyApp(App):
def build(self):
caminho_banco = f"{self.user_data_dir}/financy.db"
self.db = BancoDados(caminho_banco)
return LayoutPrincipal()
def on_stop(self):
self.db.fechar()
FinancyApp().run()
<LayoutPrincipal>:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: 70
Label:
text: "Financy"
ScreenManager:
id: telas
Screen:
name: "resumo"
Label:
text: "Tela de Resumo"
Screen:
name: "renda"
Label:
text: "Tela de Renda"
Screen:
name: "gastos_fixos"
Label:
text: "Tela de Gastos Fixos"
Screen:
name: "gastos_periodicos"
Label:
text: "Tela de Gastos Periódicos"
BoxLayout:
size_hint_y: None
height: 70
Button:
text: "Resumo"
on_release: telas.current = "resumo"
Button:
text: "Renda"
on_release: telas.current = "renda"
Button:
text: "Gastos Fixos"
on_release: telas.current = "gastos_fixos"
Button:
text: "Gastos Periódicos"
on_release: telas.current = "gastos_periodicos"
Classe para banco de dados
Antes de criar o banco de dados, precisamos entender o que o aplicativo vai fazer e quais informações ele precisa salvar. A ideia deste app é permitir o controle simples de entradas e saídas de dinheiro (por enquanto apenas isso). O usuário poderá cadastrar rendas e, depois, registrar gastos vinculados a essas rendas.
Rendas A renda representa o dinheiro que entra. Ela pode ser uma renda recorrente, como salário, bolsa, benefício ou qualquer valor recebido todos os meses. Também pode ser uma renda esporádica, como um pagamento único, um serviço avulso, uma venda ou qualquer outro valor recebido apenas uma vez.
A renda será a base para controlar quanto dinheiro o usuário possui disponível em determinado período.
Gastos O gasto representa qualquer valor que saiu da renda cadastrada. Pode ser um almoço, um café, uma compra no mercado, uma conta paga, um transporte ou qualquer outra despesa.
Todo gasto precisa estar vinculado a uma renda. Isso permite saber de onde aquele dinheiro saiu e quanto ainda resta daquela renda depois dos gastos registrados.
Assinaturas e contas fixas As assinaturas representam gastos recorrentes, ou seja, valores que se repetem com certa frequência. Elas podem ser contas fixas, como água, luz, internet e aluguel, ou serviços contratados, como Netflix, Prime Video, Spotify, armazenamento em nuvem e outros serviços parecidos.
Diferente de um gasto comum, que normalmente acontece uma única vez, a assinatura possui uma ideia de repetição. Por isso, ela precisa guardar informações como valor, data de vencimento, frequência de cobrança e se ainda está ativa.
Assim como os gastos, uma assinatura também precisa estar vinculada a uma renda, pois o valor cobrado precisa sair de alguma origem de dinheiro.
Uma ideia é que no caso de uma parcela, poder adicionar quantas vezes foi parcelado, assim você sabe quando vai acabar a parcela.
Com essa relação, o aplicativo poderá mostrar informações como o valor total recebido, o total gasto e o saldo restante.
Além disso, o App terá quatro telas, como já informado. A primeira é a tela de resumo, ela deve mostrar quanto tem de dinheiro com base nas Rendas e nos Gastos mais as Assinaturas e contas fixas.
Tabela de Renda
A tabela de renda será responsável por armazenar todas as entradas de dinheiro cadastradas no aplicativo. Cada renda precisa possuir um identificador único. Esse identificador será usado internamente pelo banco de dados para diferenciar uma renda da outra e também para permitir que gastos e assinaturas sejam vinculados a uma renda específica.
A renda também precisa ter um nome. Esse nome funciona como uma descrição simples para o usuário identificar de onde aquele dinheiro veio.
Outro campo necessário é o valor da renda. Esse campo representa quanto dinheiro entrou ou quanto dinheiro entra de forma recorrente. Para evitar problemas com arredondamento, o ideal é que esse valor seja armazenado em centavos no banco de dados.
Também é importante ter um campo para indicar se essa renda é recorrente ou não. Uma renda recorrente é aquela que se repete todos os meses, como salário, bolsa ou benefício. Já uma renda não recorrente representa uma entrada de dinheiro pontual, como um pagamento avulso, uma venda ou um serviço feito apenas uma vez.
Para organizar melhor as rendas, vamos usar um campo chamado mês de referência. Esse campo indica a qual mês e ano aquela renda pertence. O mês de referência deve armazenar tanto o ano quanto o mês, usando um formato simples, como 2026-05. Dessa forma, o aplicativo consegue saber que aquela renda pertence a maio de 2026, sem precisar controlar o dia exato em que o dinheiro entrou.
Para rendas recorrentes, o mês de referência indica a partir de qual mês aquela renda deve começar a ser considerada. Por exemplo, uma renda recorrente com mês de referência 2026-05 pode ser usada pelo aplicativo a partir de maio de 2026 e também nos meses seguintes, enquanto estiver ativa.
Para rendas não recorrentes, o mês de referência indica o único mês em que aquela renda deve ser considerada. Por exemplo, uma renda esporádica com mês de referência 2026-05 pertence apenas a maio de 2026.
Também é interessante ter um campo para indicar se a renda está ativa. Isso permite desativar uma renda sem precisar apagá-la do banco de dados. Por exemplo, se o usuário trocou de emprego ou deixou de receber determinado benefício, a renda pode ser marcada como inativa, mantendo o histórico salvo.
Além disso, a tabela pode ter um campo de observação. Esse campo não é obrigatório, mas permite que o usuário registre alguma informação adicional sobre aquela renda, como "salário líquido aproximado" ou "pagamento recebido por serviço avulso".
Por fim, vamos armazenar a data de criação e a data da última atualização do registro. Esses campos ajudam no controle interno do aplicativo e permitem saber quando a assinatura foi cadastrada ou alterada.
Abaixo deixo um resumo sobre os campos da tabela:
- id
- nome
- valor
- recorrente
- mes_referencia
- ativa
- observacao
- criado_em
- atualizado_em
Tabela de gastos
A tabela de gastos será responsável por armazenar todas as saídas de dinheiro registradas no aplicativo. Cada gasto precisa possuir um identificador único. Esse identificador será usado internamente pelo banco de dados para diferenciar um gasto do outro e facilitar consultas, edições e remoções.
Todo gasto também precisa estar vinculado a uma renda. Essa relação é importante porque, dentro da lógica do aplicativo, nenhum gasto existe sem uma origem de dinheiro, uu seja, ao cadastrar um gasto, o usuário precisa informar de qual renda aquele valor saiu.
O gasto também precisa ter um nome ou descrição. Esse campo serve para identificar o motivo daquele gasto de forma simples.
Outro campo essencial é o valor do gasto. Esse campo representa quanto dinheiro foi retirado da renda vinculada. Assim como na tabela de renda, o ideal é armazenar esse valor em centavos para evitar problemas com arredondamento.
Também é necessário armazenar a data do gasto. Esse campo indica quando o dinheiro foi gasto e permite que o aplicativo organize as despesas por dia, mês e ano. Com isso, será possível mostrar quanto foi gasto em determinado período.
A tabela também pode ter uma categoria, que será um texto. O usuario pode usar esse campo para organizar melhor os gastos, separando despesas por tipo.
Também é interessante ter um campo de observação. Esse campo permite adicionar alguma informação extra sobre o gasto, caso o usuário queira registrar mais detalhes. Por exemplo, "almoço no trabalho", "compra do mês" ou "Uber".
Por fim, vamos armazenar a data de criação e a data da última atualização do registro. Esses campos ajudam no controle interno do aplicativo e permitem saber quando a assinatura foi cadastrada ou alterada.
Abaixo deixo um resumo sobre os campos da tabela:
- id
- renda_id
- nome
- valor
- data_gasto
- categoria
- observacao
- criado_em
- atualizado_em
Tabela de assinaturas e contas fixas
A tabela de assinaturas e contas fixas será responsável por armazenar os gastos recorrentes do aplicativo. Diferente da tabela de gastos, que registra despesas pontuais, essa tabela representa valores que se repetem com frequência. Alguns exemplos são conta de água, conta de luz, internet, aluguel, Netflix, Prime Video, Spotify, armazenamento em nuvem e outros serviços parecidos.
Cada assinatura ou conta fixa precisa possuir um identificador único. Esse identificador será usado internamente pelo banco de dados para diferenciar uma assinatura da outra e permitir consultas, alterações e remoções.
Assim como os gastos comuns, uma assinatura ou conta fixa também precisa estar vinculada a uma renda. Isso acontece porque, dentro da lógica do aplicativo, todo valor que sai precisa ter uma origem. Dessa forma, o app consegue saber de qual renda aquele valor será descontado.
Também é necessário ter um nome ou descrição. Esse campo serve para identificar o serviço ou a conta cadastrada. Alguns exemplos seriam internet, conta de luz, Netflix, aluguel, plano de celular ou academia.
Outro campo essencial é o valor da assinatura ou conta fixa. Assim como nas tabelas de renda e gastos, o valor deve ser armazenado em centavos no banco de dados.
A tabela também precisa ter um campo para indicar a frequência da cobrança. Na maioria dos casos, a cobrança será mensal, mas podem existir serviços semanais, anuais ou cobranças com outro intervalo. Esse campo ajuda o aplicativo a entender quando aquele valor deve aparecer novamente.
Também é importante armazenar o dia de vencimento. Esse campo indica em qual dia do mês aquela conta ou assinatura normalmente deve ser paga. Por exemplo, uma internet que vence todo dia 10 ou uma assinatura que é cobrada todo dia 15.
Além disso, é útil ter um campo para indicar a data da primeira cobrança. Esse campo ajuda o aplicativo a saber a partir de quando aquela assinatura ou conta fixa começou a valer.
Também é quero colocar um campo para indicar se a assinatura está ativa. Isso permite manter o histórico sem precisar apagar o registro.
A tabela também precisa ter uma categoria, que será um texto. Esse campo ajuda a organizar melhor os dados. Também quero colocar um campo de observação para guardar informações adicionais.
Por fim, vamos armazenar a data de criação e a data da última atualização do registro. Esses campos ajudam no controle interno do aplicativo e permitem saber quando a assinatura foi cadastrada ou alterada.
Abaixo deixo um resumo sobre os campos da tabela:
- id
- renda_id
- nome
- valor_centavos
- frequencia
- dia_vencimento
- data_primeira_cobranca
- ativa
- categoria
- observacao
- criado_em
- atualizado_em
Criando a classe do banco
Já pensando em app de celular, o ponto principal é não fixar o caminho do banco no código, não se esqueçam disso. O ideal é a classe receber o caminho do banco, e no Kivy você passa o diretório próprio do aplicativo.
import sqlite3
from pathlib import Path
class BancoDados:
def __init__(self, caminho_banco):
self.caminho_banco = Path(caminho_banco)
self.caminho_banco.parent.mkdir(
parents=True,
exist_ok=True
)
self.conexao = sqlite3.connect(self.caminho_banco)
self.conexao.row_factory = sqlite3.Row
self.conexao.execute("PRAGMA foreign_keys = ON")
self.criar_tabelas()
def criar_tabelas(self):
cursor = self.conexao.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS rendas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
valor_centavos INTEGER NOT NULL CHECK (valor_centavos >= 0),
recorrente INTEGER NOT NULL DEFAULT 0 CHECK (recorrente IN (0, 1)),
mes_referencia TEXT NOT NULL,
ativa INTEGER NOT NULL DEFAULT 1 CHECK (ativa IN (0, 1)),
observacao TEXT,
criado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS gastos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
renda_id INTEGER NOT NULL,
nome TEXT NOT NULL,
valor_centavos INTEGER NOT NULL,
data_gasto TEXT NOT NULL,
categoria TEXT,
observacao TEXT,
criado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (renda_id)
REFERENCES rendas (id)
ON DELETE RESTRICT
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS assinaturas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
renda_id INTEGER NOT NULL,
nome TEXT NOT NULL,
valor_centavos INTEGER NOT NULL,
frequencia TEXT NOT NULL DEFAULT 'mensal',
dia_vencimento INTEGER,
data_primeira_cobranca TEXT,
ativa INTEGER NOT NULL DEFAULT 1,
categoria TEXT,
observacao TEXT,
criado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (renda_id)
REFERENCES rendas (id)
ON DELETE RESTRICT
)
""")
self.conexao.commit()
Usei ON DELETE RESTRICT porque, pela regra do app, gastos e assinaturas dependem de uma renda. Então o banco não deixa apagar uma renda que ainda possui gastos ou assinaturas vinculadas.
Podemos testar com o seguinte código:
from db import BancoDados
banco = BancoDados("financy.db")
print("Banco criado com sucesso!")
Métodos para manipular os dados
Depois de criar as tabelas do banco, o próximo passo será criar métodos para manipular os dados salvos no aplicativo. Esses métodos devem ficar dentro da classe responsável pelo banco de dados. Isso é importante porque as telas do aplicativo não devem executar comandos SQL diretamente.
A tela deve cuidar da interface, enquanto a classe do banco deve cuidar da gravação, alteração, remoção e consulta dos dados. Com essa separação, o código fica mais organizado. Se em algum momento for necessário mudar a forma como uma renda, gasto ou assinatura é salvo, a alteração será feita apenas na classe do banco, sem precisar modificar várias telas do aplicativo.
A mesma ideia vale para gastos e assinaturas. A tela coleta as informações, mas quem realmente salva, atualiza ou remove os dados é a classe do banco. Em vez de criar apenas métodos como inserir_dados, remover_dados e atualizar_dados, vou criar métodos específicos para cada tipo de informação, como:
adicionar_renda
atualizar_renda
remover_renda
adicionar_gasto
atualizar_gasto
remover_gasto
adicionar_assinatura
atualizar_assinatura
remover_assinatura
Métodos de Renda
Vamos começar criando os métodos para trabalhar com a Renda.
Métodos para Adicionar Renda
def adicionar_renda(
self,
nome,
valor_centavos,
mes_referencia,
recorrente=False,
ativa=True,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
INSERT INTO rendas (
nome,
valor_centavos,
recorrente,
mes_referencia,
ativa,
observacao
)
VALUES (?, ?, ?, ?, ?, ?)
""", (
nome,
valor_centavos,
int(recorrente),
mes_referencia,
int(ativa),
observacao
))
self.conexao.commit()
# cursor.lastrowid retorna o ID do último registro inserido pelo INSERT:
return cursor.lastrowid
Métodos para Alterar Renda
def atualizar_renda(
self,
renda_id,
nome,
valor_centavos,
mes_referencia,
recorrente=False,
ativa=True,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE rendas
SET
nome = ?,
valor_centavos = ?,
recorrente = ?,
mes_referencia = ?,
ativa = ?,
observacao = ?,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (
nome,
valor_centavos,
int(recorrente),
mes_referencia,
int(ativa),
observacao,
renda_id
))
self.conexao.commit()
# retorna quantas linhas foram afetadas pelo último comando SQL executado:
return cursor.rowcount
Métodos para Desativar Renda
def desativar_renda(self, renda_id):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE rendas
SET
ativa = 0,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (renda_id,))
self.conexao.commit()
# retorna quantas linhas foram afetadas pelo último comando SQL executado:
return cursor.rowcount
Métodos para Ativar Renda
def ativar_renda(self, renda_id):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE rendas
SET
ativa = 1,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (renda_id,))
self.conexao.commit()
# retorna quantas linhas foram afetadas pelo último comando SQL executado:
return cursor.rowcount
Métodos para Apagar Renda
def remover_renda(self, renda_id):
cursor = self.conexao.cursor()
cursor.execute("""
DELETE FROM rendas
WHERE id = ?
""", (renda_id,))
self.conexao.commit()
# retorna quantas linhas foram afetadas pelo último comando SQL executado:
return cursor.rowcount
Métodos de Gastos
Agora vamos criar os métodos para trabalhar com os gastos. Diferente da renda, o gasto precisa estar vinculado a uma renda existente. Por isso, os métodos de gasto precisam receber o campo renda_id, que representa a renda de onde aquele dinheiro saiu.
Métodos para Adicionar Gasto
def adicionar_gasto(
self,
renda_id,
nome,
valor_centavos,
data_gasto,
categoria=None,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
INSERT INTO gastos (
renda_id,
nome,
valor_centavos,
data_gasto,
categoria,
observacao
)
VALUES (?, ?, ?, ?, ?, ?)
""", (
renda_id,
nome,
valor_centavos,
data_gasto,
categoria,
observacao
))
self.conexao.commit()
# cursor.lastrowid retorna o ID do último registro inserido pelo INSERT:
return cursor.lastrowid
Métodos para Alterar Gasto
def atualizar_gasto(
self,
gasto_id,
renda_id,
nome,
valor_centavos,
data_gasto,
categoria=None,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE gastos
SET
renda_id = ?,
nome = ?,
valor_centavos = ?,
data_gasto = ?,
categoria = ?,
observacao = ?,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (
renda_id,
nome,
valor_centavos,
data_gasto,
categoria,
observacao,
gasto_id
))
self.conexao.commit()
# retorna quantas linhas foram afetadas pelo último comando SQL executado:
return cursor.rowcount
Métodos para Apagar Gasto
def remover_gasto(self, gasto_id):
cursor = self.conexao.cursor()
cursor.execute("""
DELETE FROM gastos
WHERE id = ?
""", (gasto_id,))
self.conexao.commit()
# retorna quantas linhas foram afetadas pelo último comando SQL executado:
return cursor.rowcount
No caso dos gastos, não criamos métodos para ativar ou desativar porque a tabela de gastos não possui o campo ativa. A ideia é que o gasto represente uma saída de dinheiro que realmente aconteceu. Então, se o usuário cadastrou um gasto errado, ele pode alterar esse registro ou removê-lo.
O campo mais importante desses métodos é o renda_id, porque ele mantém a ligação entre o gasto e a renda. Como o banco possui uma chave estrangeira, o SQLite só deve aceitar o cadastro de um gasto se a renda informada existir.
Métodos de Assinaturas e Contas Fixas
Agora vamos criar os métodos para trabalhar com assinaturas e contas fixas. Essa tabela será usada para guardar gastos recorrentes, como internet, aluguel, conta de luz, água, Netflix, Prime Video, Spotify e outros serviços ou contas que se repetem com frequência.
Assim como os gastos comuns, uma assinatura ou conta fixa também precisa estar vinculada a uma renda. Por isso, os métodos também recebem o campo renda_id.
Métodos para Adicionar Assinatura
def adicionar_assinatura(
self,
renda_id,
nome,
valor_centavos,
frequencia="mensal",
dia_vencimento=None,
data_primeira_cobranca=None,
ativa=True,
categoria=None,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
INSERT INTO assinaturas (
renda_id,
nome,
valor_centavos,
frequencia,
dia_vencimento,
data_primeira_cobranca,
ativa,
categoria,
observacao
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
renda_id,
nome,
valor_centavos,
frequencia,
dia_vencimento,
data_primeira_cobranca,
int(ativa),
categoria,
observacao
))
self.conexao.commit()
# cursor.lastrowid retorna o ID do último registro inserido pelo INSERT:
return cursor.lastrowid
Métodos para Alterar Assinatura
def atualizar_assinatura(
self,
assinatura_id,
renda_id,
nome,
valor_centavos,
frequencia="mensal",
dia_vencimento=None,
data_primeira_cobranca=None,
ativa=True,
categoria=None,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE assinaturas
SET
renda_id = ?,
nome = ?,
valor_centavos = ?,
frequencia = ?,
dia_vencimento = ?,
data_primeira_cobranca = ?,
ativa = ?,
categoria = ?,
observacao = ?,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (
renda_id,
nome,
valor_centavos,
frequencia,
dia_vencimento,
data_primeira_cobranca,
int(ativa),
categoria,
observacao,
assinatura_id
))
self.conexao.commit()
# retorna quantas linhas foram afetadas pelo último comando SQL executado:
return cursor.rowcount
Métodos para Desativar Assinatura
def desativar_assinatura(self, assinatura_id):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE assinaturas
SET
ativa = 0,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (assinatura_id,))
self.conexao.commit()
# retorna quantas linhas foram afetadas pelo último comando SQL executado:
return cursor.rowcount
Métodos para Ativar Assinatura
def ativar_assinatura(self, assinatura_id):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE assinaturas
SET
ativa = 1,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (assinatura_id,))
self.conexao.commit()
# retorna quantas linhas foram afetadas pelo último comando SQL executado:
return cursor.rowcount
Métodos para Apagar Assinatura
def remover_assinatura(self, assinatura_id):
cursor = self.conexao.cursor()
cursor.execute("""
DELETE FROM assinaturas
WHERE id = ?
""", (assinatura_id,))
self.conexao.commit()
# retorna quantas linhas foram afetadas pelo último comando SQL executado:
return cursor.rowcount
Métodos para consultar dados
Além dos métodos para adicionar, alterar, ativar, desativar e remover dados, também precisamos criar métodos para consultar as informações salvas no banco. Esses métodos serão usados pelas telas do aplicativo para carregar dados já cadastrados.
Por exemplo, quando o usuário acessar a tela de rendas, o app precisará listar todas as rendas cadastradas. Quando ele quiser editar uma renda, o app precisará buscar aquela renda pelo ID e carregar suas informações no formulário.
A mesma lógica vale para gastos e assinaturas. O aplicativo precisa conseguir listar todos os registros, buscar um registro específico e também listar os dados relacionados a uma renda.
def listar(self, tipo, apenas_ativos=False, renda_id=None):
consultas = {
"rendas": {
"tabela": "rendas",
"colunas": """
id,
nome,
valor_centavos,
recorrente,
dia_recebimento,
data_recebimento,
ativa,
observacao,
criado_em,
atualizado_em
""",
"ordem": "nome",
"possui_ativa": True,
"possui_renda_id": False
},
"gastos": {
"tabela": "gastos",
"colunas": """
id,
renda_id,
nome,
valor_centavos,
data_gasto,
categoria,
observacao,
criado_em,
atualizado_em
""",
"ordem": "data_gasto DESC",
"possui_ativa": False,
"possui_renda_id": True
},
"assinaturas": {
"tabela": "assinaturas",
"colunas": """
id,
renda_id,
nome,
valor_centavos,
frequencia,
dia_vencimento,
data_primeira_cobranca,
ativa,
categoria,
observacao,
criado_em,
atualizado_em
""",
"ordem": "nome",
"possui_ativa": True,
"possui_renda_id": True
}
}
if tipo not in consultas:
raise ValueError(
"Tipo inválido. Use: rendas, gastos ou assinaturas."
)
config = consultas[tipo]
filtros = []
parametros = []
if apenas_ativos and config["possui_ativa"]:
filtros.append("ativa = 1")
if renda_id is not None:
if not config["possui_renda_id"]:
raise ValueError(
"Esse tipo de consulta não usa renda_id."
)
filtros.append("renda_id = ?")
parametros.append(renda_id)
where = ""
if filtros:
where = "WHERE " + " AND ".join(filtros)
sql = f"""
SELECT
{config["colunas"]}
FROM {config["tabela"]}
{where}
ORDER BY {config["ordem"]}
"""
cursor = self.conexao.cursor()
cursor.execute(sql, parametros)
return cursor.fetchall()
Método para buscar dados baseado em ID
O método listar() resolve bem para mostrar várias rendas, gastos ou assinaturas. Mas, quando o usuário quiser editar um registro, o app precisa carregar apenas aquele item específico. Para isso, vamos criar mais um método.
def buscar_por_id(self, tipo, registro_id):
consultas = {
"rendas": {
"tabela": "rendas",
"colunas": """
id,
nome,
valor_centavos,
recorrente,
dia_recebimento,
data_recebimento,
ativa,
observacao,
criado_em,
atualizado_em
"""
},
"gastos": {
"tabela": "gastos",
"colunas": """
id,
renda_id,
nome,
valor_centavos,
data_gasto,
categoria,
observacao,
criado_em,
atualizado_em
"""
},
"assinaturas": {
"tabela": "assinaturas",
"colunas": """
id,
renda_id,
nome,
valor_centavos,
frequencia,
dia_vencimento,
data_primeira_cobranca,
ativa,
categoria,
observacao,
criado_em,
atualizado_em
"""
}
}
if tipo not in consultas:
raise ValueError(
"Tipo inválido. Use: rendas, gastos ou assinaturas."
)
config = consultas[tipo]
sql = f"""
SELECT
{config["colunas"]}
FROM {config["tabela"]}
WHERE id = ?
"""
cursor = self.conexao.cursor()
cursor.execute(sql, (registro_id,))
return cursor.fetchone()
Nova tabela para parcelas
Em cima da hora eu tive a ideia de poder cadastrar parcelas e acho que seria melhor colocar esses gastos em uma tabela separada. Ao invés de misturar esse tipo de informação com os gastos comuns, vamos criar uma tabela separada para parcelas. Isso deixa mais organizado, porque os gastos esporádicos ficam em uma tabela e as compras parceladas ficam em outra.
Eu já alterei o main.py para ter as mudanças necessárias, então nesse ponto, você já deve ter ele com as mudanças necessárias.
Com essa separação, o aplicativo poderá mostrar os gastos comuns de um lado e as parcelas de outro. Isso também facilita os cálculos mensais, já que cada parcela terá sua própria data, valor e vínculo com uma renda.
A tabela de parcelas será usada para representar compras divididas em várias partes, como uma compra em 3x, 6x, 10x ou qualquer outra quantidade de parcelas. Primeiro temos que criar a estrutura da tabela. Cada parcela precisa possuir um identificador único. Esse identificador será usado internamente pelo banco de dados para diferenciar uma parcela da outra e permitir consultas, alterações e remoções.
Toda parcela também precisa estar vinculada a uma renda. Essa relação é importante porque, dentro da lógica do aplicativo, todo valor que sai precisa ter uma origem. A parcela também precisa ter um nome ou descrição. Esse campo serve para identificar a compra parcelada de forma simples.
Também precisamos de um campo para identificar o grupo do parcelamento. Esse campo serve para indicar que várias parcelas fazem parte da mesma compra. Por exemplo, uma compra feita em 10 vezes terá 10 registros na tabela de parcelas, mas todos eles terão o mesmo grupo de parcelamento.
Outro campo essencial é o valor da parcela. Esse campo representa o valor de uma parcela específica. Assim como nas outras tabelas, o ideal é armazenar esse valor em centavos no banco de dados para evitar problemas com arredondamento.
Também é necessário armazenar o número da parcela. Esse campo indica qual parcela aquele registro representa. Por exemplo, em uma compra feita em 10 vezes, teremos a parcela 1, parcela 2, parcela 3 e assim por diante.
Além disso, precisamos armazenar o total de parcelas. Esse campo indica em quantas vezes a compra foi dividida. Assim, o aplicativo consegue mostrar informações como “parcela 3 de 10”.
Também precisamos armazenar a data da parcela. Esse campo indica em qual data aquela parcela deve ser considerada. Com isso, o aplicativo consegue organizar as parcelas por mês e incluir o valor no cálculo correto.
A tabela também vai ter um campo para indicar se a parcela foi paga ou não. Isso permite controlar quais parcelas ainda estão pendentes e quais já foram quitadas.
Quando uma parcela for marcada como paga, também podemos armazenar a data de pagamento. Esse campo pode ficar vazio enquanto a parcela estiver pendente e ser preenchido apenas quando o usuário informar que ela foi paga.
Também vamos colocar um campo de observação. Esse campo permite adicionar alguma informação extra sobre a parcela ou sobre a compra parcelada. A tabela também vai ter uma categoria, que será um texto.
Por fim, vamos armazenar a data de criação e a data da última atualização do registro. Esses campos ajudam no controle interno do aplicativo e permitem saber quando a parcela foi cadastrada ou alterada.
Abaixo deixo um resumo sobre os campos da tabela:
- id
- renda_id
- grupo_parcelamento
- nome
- valor_centavos
- numero_parcela
- total_parcelas
- data_parcela
- paga
- data_pagamento
- categoria
- observacao
- criado_em
- atualizado_em
Abaixo segue o código para criar a tabela:
cursor.execute("""
CREATE TABLE IF NOT EXISTS parcelas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
renda_id INTEGER NOT NULL,
grupo_parcelamento TEXT NOT NULL,
nome TEXT NOT NULL,
valor_centavos INTEGER NOT NULL CHECK (valor_centavos >= 0),
numero_parcela INTEGER NOT NULL CHECK (numero_parcela >= 1),
total_parcelas INTEGER NOT NULL CHECK (total_parcelas >= 1),
data_parcela TEXT NOT NULL,
paga INTEGER NOT NULL DEFAULT 0 CHECK (paga IN (0, 1)),
data_pagamento TEXT,
categoria TEXT,
observacao TEXT,
criado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (renda_id)
REFERENCES rendas (id)
ON DELETE RESTRICT
)
""")
Métodos de Parcelas
Agora vamos criar os métodos para trabalhar com parcelas. Cada parcela será salva como um registro separado no banco, mas todas as parcelas da mesma compra terão o mesmo grupo_parcelamento.
Isso permite que o aplicativo mostre cada parcela no mês correto e, ao mesmo tempo, saiba que todas pertencem à mesma compra parcelada.
Primeiro, adicione os imports abaixo no começo do db.py:
from calendar import monthrange
from datetime import date, datetime
from uuid import uuid4
Métodos Somas mês
O método abaixo serve para calcular a data de cada parcela.
def _somar_meses(self, data_base, meses):
data = datetime.strptime(data_base, "%Y-%m-%d").date()
mes = data.month - 1 + meses
ano = data.year + mes // 12
mes = mes % 12 + 1
ultimo_dia_mes = monthrange(ano, mes)[1]
dia = min(data.day, ultimo_dia_mes)
return date(ano, mes, dia).isoformat()
Método para Adicionar Parcelas
def adicionar_parcelas(
self,
renda_id,
nome,
valor_centavos,
total_parcelas,
data_primeira_parcela,
categoria=None,
observacao=None,
grupo_parcelamento=None
):
if total_parcelas < 1:
raise ValueError("O total de parcelas precisa ser maior que zero.")
if valor_centavos < 0:
raise ValueError("O valor da parcela não pode ser negativo.")
if grupo_parcelamento is None:
grupo_parcelamento = f"parcelamento-{uuid4().hex}"
cursor = self.conexao.cursor()
parcelas_ids = []
try:
for numero_parcela in range(1, total_parcelas + 1):
data_parcela = self._somar_meses(
data_primeira_parcela,
numero_parcela - 1
)
cursor.execute("""
INSERT INTO parcelas (
renda_id,
grupo_parcelamento,
nome,
valor_centavos,
numero_parcela,
total_parcelas,
data_parcela,
paga,
data_pagamento,
categoria,
observacao
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
renda_id,
grupo_parcelamento,
nome,
valor_centavos,
numero_parcela,
total_parcelas,
data_parcela,
0,
None,
categoria,
observacao
))
parcelas_ids.append(cursor.lastrowid)
self.conexao.commit()
return {
"grupo_parcelamento": grupo_parcelamento,
"parcelas_ids": parcelas_ids
}
except sqlite3.Error:
self.conexao.rollback()
raise
Método para Alterar Parcela
def atualizar_parcela(
self,
parcela_id,
renda_id,
grupo_parcelamento,
nome,
valor_centavos,
numero_parcela,
total_parcelas,
data_parcela,
paga=False,
data_pagamento=None,
categoria=None,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE parcelas
SET
renda_id = ?,
grupo_parcelamento = ?,
nome = ?,
valor_centavos = ?,
numero_parcela = ?,
total_parcelas = ?,
data_parcela = ?,
paga = ?,
data_pagamento = ?,
categoria = ?,
observacao = ?,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (
renda_id,
grupo_parcelamento,
nome,
valor_centavos,
numero_parcela,
total_parcelas,
data_parcela,
int(paga),
data_pagamento,
categoria,
observacao,
parcela_id
))
self.conexao.commit()
return cursor.rowcount
Método para Marcar Parcela como Pendente
def marcar_parcela_como_pendente(self, parcela_id):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE parcelas
SET
paga = 0,
data_pagamento = NULL,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (parcela_id,))
self.conexao.commit()
return cursor.rowcount
Método para Apagar uma Parcela
Esse método remove apenas uma parcela específica.
def remover_parcela(self, parcela_id):
cursor = self.conexao.cursor()
cursor.execute("""
DELETE FROM parcelas
WHERE id = ?
""", (parcela_id,))
self.conexao.commit()
return cursor.rowcount
Método para Apagar um Parcelamento Inteiro
Esse método remove todas as parcelas que pertencem ao mesmo parcelamento.
def remover_parcelamento(self, grupo_parcelamento):
cursor = self.conexao.cursor()
cursor.execute("""
DELETE FROM parcelas
WHERE grupo_parcelamento = ?
""", (grupo_parcelamento,))
self.conexao.commit()
return cursor.rowcount
Cógido completo do db.py até agora
import sqlite3
from pathlib import Path
from calendar import monthrange
from datetime import date, datetime
from uuid import uuid4
class BancoDados:
def __init__(self, caminho_banco):
self.caminho_banco = Path(caminho_banco)
self.caminho_banco.parent.mkdir(
parents=True,
exist_ok=True
)
self.conexao = sqlite3.connect(str(self.caminho_banco))
self.conexao.row_factory = sqlite3.Row
self.conexao.execute("PRAGMA foreign_keys = ON")
self.criar_tabelas()
def criar_tabelas(self):
cursor = self.conexao.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS rendas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
valor_centavos INTEGER NOT NULL CHECK (valor_centavos >= 0),
recorrente INTEGER NOT NULL DEFAULT 0 CHECK (recorrente IN (0, 1)),
mes_referencia TEXT NOT NULL,
ativa INTEGER NOT NULL DEFAULT 1 CHECK (ativa IN (0, 1)),
observacao TEXT,
criado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS gastos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
renda_id INTEGER NOT NULL,
nome TEXT NOT NULL,
valor_centavos INTEGER NOT NULL CHECK (valor_centavos >= 0),
data_gasto TEXT NOT NULL,
categoria TEXT,
observacao TEXT,
criado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (renda_id)
REFERENCES rendas (id)
ON DELETE RESTRICT
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS assinaturas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
renda_id INTEGER NOT NULL,
nome TEXT NOT NULL,
valor_centavos INTEGER NOT NULL CHECK (valor_centavos >= 0),
frequencia TEXT NOT NULL DEFAULT 'mensal',
dia_vencimento INTEGER CHECK (dia_vencimento BETWEEN 1 AND 31),
data_primeira_cobranca TEXT,
ativa INTEGER NOT NULL DEFAULT 1 CHECK (ativa IN (0, 1)),
categoria TEXT,
observacao TEXT,
criado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (renda_id)
REFERENCES rendas (id)
ON DELETE RESTRICT
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS parcelas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
renda_id INTEGER NOT NULL,
grupo_parcelamento TEXT NOT NULL,
nome TEXT NOT NULL,
valor_centavos INTEGER NOT NULL CHECK (valor_centavos >= 0),
numero_parcela INTEGER NOT NULL CHECK (numero_parcela >= 1),
total_parcelas INTEGER NOT NULL CHECK (total_parcelas >= 1),
data_parcela TEXT NOT NULL,
paga INTEGER NOT NULL DEFAULT 0 CHECK (paga IN (0, 1)),
data_pagamento TEXT,
categoria TEXT,
observacao TEXT,
criado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (renda_id)
REFERENCES rendas (id)
ON DELETE RESTRICT
)
""")
self.conexao.commit()
def adicionar_renda(
self,
nome,
valor_centavos,
mes_referencia,
recorrente=False,
ativa=True,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
INSERT INTO rendas (
nome,
valor_centavos,
recorrente,
mes_referencia,
ativa,
observacao
)
VALUES (?, ?, ?, ?, ?, ?)
""", (
nome,
valor_centavos,
int(recorrente),
mes_referencia,
int(ativa),
observacao
))
self.conexao.commit()
return cursor.lastrowid
def atualizar_renda(
self,
renda_id,
nome,
valor_centavos,
mes_referencia,
recorrente=False,
ativa=True,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE rendas
SET
nome = ?,
valor_centavos = ?,
recorrente = ?,
mes_referencia = ?,
ativa = ?,
observacao = ?,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (
nome,
valor_centavos,
int(recorrente),
mes_referencia,
int(ativa),
observacao,
renda_id
))
self.conexao.commit()
return cursor.rowcount
def desativar_renda(self, renda_id):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE rendas
SET
ativa = 0,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (renda_id,))
self.conexao.commit()
return cursor.rowcount
def ativar_renda(self, renda_id):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE rendas
SET
ativa = 1,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (renda_id,))
self.conexao.commit()
return cursor.rowcount
def remover_renda(self, renda_id):
cursor = self.conexao.cursor()
cursor.execute("""
DELETE FROM rendas
WHERE id = ?
""", (renda_id,))
self.conexao.commit()
return cursor.rowcount
def adicionar_gasto(
self,
renda_id,
nome,
valor_centavos,
data_gasto,
categoria=None,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
INSERT INTO gastos (
renda_id,
nome,
valor_centavos,
data_gasto,
categoria,
observacao
)
VALUES (?, ?, ?, ?, ?, ?)
""", (
renda_id,
nome,
valor_centavos,
data_gasto,
categoria,
observacao
))
self.conexao.commit()
return cursor.lastrowid
def atualizar_gasto(
self,
gasto_id,
renda_id,
nome,
valor_centavos,
data_gasto,
categoria=None,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE gastos
SET
renda_id = ?,
nome = ?,
valor_centavos = ?,
data_gasto = ?,
categoria = ?,
observacao = ?,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (
renda_id,
nome,
valor_centavos,
data_gasto,
categoria,
observacao,
gasto_id
))
self.conexao.commit()
return cursor.rowcount
def remover_gasto(self, gasto_id):
cursor = self.conexao.cursor()
cursor.execute("""
DELETE FROM gastos
WHERE id = ?
""", (gasto_id,))
self.conexao.commit()
return cursor.rowcount
def adicionar_assinatura(
self,
renda_id,
nome,
valor_centavos,
frequencia="mensal",
dia_vencimento=None,
data_primeira_cobranca=None,
ativa=True,
categoria=None,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
INSERT INTO assinaturas (
renda_id,
nome,
valor_centavos,
frequencia,
dia_vencimento,
data_primeira_cobranca,
ativa,
categoria,
observacao
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
renda_id,
nome,
valor_centavos,
frequencia,
dia_vencimento,
data_primeira_cobranca,
int(ativa),
categoria,
observacao
))
self.conexao.commit()
return cursor.lastrowid
def atualizar_assinatura(
self,
assinatura_id,
renda_id,
nome,
valor_centavos,
frequencia="mensal",
dia_vencimento=None,
data_primeira_cobranca=None,
ativa=True,
categoria=None,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE assinaturas
SET
renda_id = ?,
nome = ?,
valor_centavos = ?,
frequencia = ?,
dia_vencimento = ?,
data_primeira_cobranca = ?,
ativa = ?,
categoria = ?,
observacao = ?,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (
renda_id,
nome,
valor_centavos,
frequencia,
dia_vencimento,
data_primeira_cobranca,
int(ativa),
categoria,
observacao,
assinatura_id
))
self.conexao.commit()
return cursor.rowcount
def desativar_assinatura(self, assinatura_id):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE assinaturas
SET
ativa = 0,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (assinatura_id,))
self.conexao.commit()
return cursor.rowcount
def ativar_assinatura(self, assinatura_id):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE assinaturas
SET
ativa = 1,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (assinatura_id,))
self.conexao.commit()
return cursor.rowcount
def remover_assinatura(self, assinatura_id):
cursor = self.conexao.cursor()
cursor.execute("""
DELETE FROM assinaturas
WHERE id = ?
""", (assinatura_id,))
self.conexao.commit()
return cursor.rowcount
def _somar_meses(self, data_base, meses):
data = datetime.strptime(data_base, "%Y-%m-%d").date()
mes = data.month - 1 + meses
ano = data.year + mes // 12
mes = mes % 12 + 1
ultimo_dia_mes = monthrange(ano, mes)[1]
dia = min(data.day, ultimo_dia_mes)
return date(ano, mes, dia).isoformat()
def adicionar_parcelas(
self,
renda_id,
nome,
valor_centavos,
total_parcelas,
data_primeira_parcela,
categoria=None,
observacao=None,
grupo_parcelamento=None
):
if total_parcelas < 1:
raise ValueError("O total de parcelas precisa ser maior que zero.")
if valor_centavos < 0:
raise ValueError("O valor da parcela não pode ser negativo.")
if grupo_parcelamento is None:
grupo_parcelamento = f"parcelamento-{uuid4().hex}"
cursor = self.conexao.cursor()
parcelas_ids = []
try:
for numero_parcela in range(1, total_parcelas + 1):
data_parcela = self._somar_meses(
data_primeira_parcela,
numero_parcela - 1
)
cursor.execute("""
INSERT INTO parcelas (
renda_id,
grupo_parcelamento,
nome,
valor_centavos,
numero_parcela,
total_parcelas,
data_parcela,
paga,
data_pagamento,
categoria,
observacao
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
renda_id,
grupo_parcelamento,
nome,
valor_centavos,
numero_parcela,
total_parcelas,
data_parcela,
0,
None,
categoria,
observacao
))
parcelas_ids.append(cursor.lastrowid)
self.conexao.commit()
return {
"grupo_parcelamento": grupo_parcelamento,
"parcelas_ids": parcelas_ids
}
except sqlite3.Error:
self.conexao.rollback()
raise
def atualizar_parcela(
self,
parcela_id,
renda_id,
grupo_parcelamento,
nome,
valor_centavos,
numero_parcela,
total_parcelas,
data_parcela,
paga=False,
data_pagamento=None,
categoria=None,
observacao=None
):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE parcelas
SET
renda_id = ?,
grupo_parcelamento = ?,
nome = ?,
valor_centavos = ?,
numero_parcela = ?,
total_parcelas = ?,
data_parcela = ?,
paga = ?,
data_pagamento = ?,
categoria = ?,
observacao = ?,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (
renda_id,
grupo_parcelamento,
nome,
valor_centavos,
numero_parcela,
total_parcelas,
data_parcela,
int(paga),
data_pagamento,
categoria,
observacao,
parcela_id
))
self.conexao.commit()
return cursor.rowcount
def marcar_parcela_como_paga(self, parcela_id, data_pagamento=None):
if data_pagamento is None:
data_pagamento = date.today().isoformat()
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE parcelas
SET
paga = 1,
data_pagamento = ?,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (
data_pagamento,
parcela_id
))
self.conexao.commit()
return cursor.rowcount
def marcar_parcela_como_pendente(self, parcela_id):
cursor = self.conexao.cursor()
cursor.execute("""
UPDATE parcelas
SET
paga = 0,
data_pagamento = NULL,
atualizado_em = CURRENT_TIMESTAMP
WHERE id = ?
""", (parcela_id,))
self.conexao.commit()
return cursor.rowcount
def remover_parcela(self, parcela_id):
cursor = self.conexao.cursor()
cursor.execute("""
DELETE FROM parcelas
WHERE id = ?
""", (parcela_id,))
self.conexao.commit()
return cursor.rowcount
def remover_parcelamento(self, grupo_parcelamento):
cursor = self.conexao.cursor()
cursor.execute("""
DELETE FROM parcelas
WHERE grupo_parcelamento = ?
""", (grupo_parcelamento,))
self.conexao.commit()
return cursor.rowcount
def listar(self, tipo, apenas_ativos=False, renda_id=None):
consultas = {
"rendas": {
"tabela": "rendas",
"colunas": """
id,
nome,
valor_centavos,
recorrente,
mes_referencia,
ativa,
observacao,
criado_em,
atualizado_em
""",
"ordem": "nome",
"possui_ativa": True,
"possui_renda_id": False
},
"gastos": {
"tabela": "gastos",
"colunas": """
id,
renda_id,
nome,
valor_centavos,
data_gasto,
categoria,
observacao,
criado_em,
atualizado_em
""",
"ordem": "data_gasto DESC",
"possui_ativa": False,
"possui_renda_id": True
},
"assinaturas": {
"tabela": "assinaturas",
"colunas": """
id,
renda_id,
nome,
valor_centavos,
frequencia,
dia_vencimento,
data_primeira_cobranca,
ativa,
categoria,
observacao,
criado_em,
atualizado_em
""",
"ordem": "nome",
"possui_ativa": True,
"possui_renda_id": True
},
"parcelas": {
"tabela": "parcelas",
"colunas": """
id,
renda_id,
grupo_parcelamento,
nome,
valor_centavos,
numero_parcela,
total_parcelas,
data_parcela,
paga,
data_pagamento,
categoria,
observacao,
criado_em,
atualizado_em
""",
"ordem": "data_parcela ASC, numero_parcela ASC",
"possui_ativa": False,
"possui_renda_id": True
}
}
if tipo not in consultas:
raise ValueError(
"Tipo inválido. Use: rendas, gastos, assinaturas ou parcelas."
)
config = consultas[tipo]
filtros = []
parametros = []
if apenas_ativos and config["possui_ativa"]:
filtros.append("ativa = 1")
if renda_id is not None:
if not config["possui_renda_id"]:
raise ValueError(
"Esse tipo de consulta não usa renda_id."
)
filtros.append("renda_id = ?")
parametros.append(renda_id)
where = ""
if filtros:
where = "WHERE " + " AND ".join(filtros)
sql = f"""
SELECT
{config["colunas"]}
FROM {config["tabela"]}
{where}
ORDER BY {config["ordem"]}
"""
cursor = self.conexao.cursor()
cursor.execute(sql, parametros)
return cursor.fetchall()
def buscar_por_id(self, tipo, registro_id):
consultas = {
"rendas": {
"tabela": "rendas",
"colunas": """
id,
nome,
valor_centavos,
recorrente,
mes_referencia,
ativa,
observacao,
criado_em,
atualizado_em
"""
},
"gastos": {
"tabela": "gastos",
"colunas": """
id,
renda_id,
nome,
valor_centavos,
data_gasto,
categoria,
observacao,
criado_em,
atualizado_em
"""
},
"assinaturas": {
"tabela": "assinaturas",
"colunas": """
id,
renda_id,
nome,
valor_centavos,
frequencia,
dia_vencimento,
data_primeira_cobranca,
ativa,
categoria,
observacao,
criado_em,
atualizado_em
"""
},
"parcelas": {
"tabela": "parcelas",
"colunas": """
id,
renda_id,
grupo_parcelamento,
nome,
valor_centavos,
numero_parcela,
total_parcelas,
data_parcela,
paga,
data_pagamento,
categoria,
observacao,
criado_em,
atualizado_em
"""
}
}
if tipo not in consultas:
raise ValueError(
"Tipo inválido. Use: rendas, gastos, assinaturas ou parcelas."
)
config = consultas[tipo]
sql = f"""
SELECT
{config["colunas"]}
FROM {config["tabela"]}
WHERE id = ?
"""
cursor = self.conexao.cursor()
cursor.execute(sql, (registro_id,))
return cursor.fetchone()
def fechar(self):
self.conexao.close()
Desenhando a tela
#:import dp kivy.metrics.dp
<LayoutPrincipal>:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(12)
Label:
text: "Financy"
font_size: "22sp"
bold: True
halign: "left"
valign: "middle"
text_size: self.size
ScreenManager:
id: gerenciador_telas
TelaResumo:
name: "resumo"
TelaRenda:
name: "renda"
TelaGastos:
name: "gastos"
TelaAssinaturas:
name: "assinaturas"
TelaParcelas:
name: "parcelas"
BoxLayout:
size_hint_y: None
height: dp(70)
spacing: dp(4)
padding: dp(4)
Button:
text: "Resumo"
font_size: "12sp"
on_release: gerenciador_telas.current = "resumo"
Button:
text: "Renda"
font_size: "12sp"
on_release: gerenciador_telas.current = "renda"
Button:
text: "Gastos"
font_size: "12sp"
on_release: gerenciador_telas.current = "gastos"
Button:
text: "Fixos"
font_size: "12sp"
on_release: gerenciador_telas.current = "assinaturas"
Button:
text: "Parcelas"
font_size: "12sp"
on_release: gerenciador_telas.current = "parcelas"
<TelaResumo>:
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
Label:
text: "Resumo"
font_size: "24sp"
bold: True
size_hint_y: None
height: dp(40)
Label:
text: "Aqui será exibido o resumo geral do aplicativo."
font_size: "16sp"
<TelaRenda>:
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
Label:
text: "Rendas"
font_size: "24sp"
bold: True
size_hint_y: None
height: dp(40)
Label:
text: "Aqui serão exibidas as rendas cadastradas."
font_size: "16sp"
<TelaGastos>:
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
Label:
text: "Gastos"
font_size: "24sp"
bold: True
size_hint_y: None
height: dp(40)
Label:
text: "Aqui serão exibidos os gastos comuns."
font_size: "16sp"
<TelaAssinaturas>:
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
Label:
text: "Assinaturas e Contas Fixas"
font_size: "24sp"
bold: True
size_hint_y: None
height: dp(40)
Label:
text: "Aqui serão exibidas as assinaturas e contas fixas."
font_size: "16sp"
<TelaParcelas>:
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
Label:
text: "Parcelas"
font_size: "24sp"
bold: True
size_hint_y: None
height: dp(40)
Label:
text: "Aqui serão exibidas as compras parceladas."
font_size: "16sp"
ScrollView
Do jeito o código está agora, não teremos a rolagem quando tivermos muitos dados na tela. O Kivy não adiciona rolagem automaticamente quando o conteúdo fica maior que a tela. Para a rolagem funcionar, precisamos colocar o conteúdo da tela dentro de um ScrollView.
A boa notícia para nós, é que o rodapé continua fixo, porque o ScrollView fica dentro da tela, e não envolvendo o LayoutPrincipal inteiro.
<TelaRenda>:
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
Label:
text: "Rendas"
font_size: "24sp"
bold: True
size_hint_y: None
height: dp(40)
Label:
text: "Aqui serão exibidas as rendas cadastradas."
font_size: "16sp"
size_hint_y: None
height: dp(40)
Button:
text: "Cadastrar nova renda"
size_hint_y: None
height: dp(48)
O ponto principal é o size_hint_y: None e height: self.minimum_height. Eles fazem o BoxLayout crescer conforme os itens internos aumentam. Aí o ScrollView consegue perceber que o conteúdo passou do tamanho visível da tela e libera a rolagem.
Segue o modelo completo:
#:import dp kivy.metrics.dp
<LayoutPrincipal>:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(8)
spacing: dp(8)
Label:
text: "Financy"
font_size: "22sp"
bold: True
halign: "left"
valign: "middle"
text_size: self.size
ScreenManager:
id: gerenciador_telas
TelaResumo:
name: "resumo"
TelaRenda:
name: "renda"
TelaGastos:
name: "gastos"
TelaAssinaturas:
name: "assinaturas"
TelaParcelas:
name: "parcelas"
BoxLayout:
size_hint_y: None
height: dp(70)
spacing: dp(4)
padding: dp(4)
canvas.before:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
Button:
text: "Resumo"
font_size: "12sp"
on_release: gerenciador_telas.current = "resumo"
Button:
text: "Renda"
font_size: "12sp"
on_release: gerenciador_telas.current = "renda"
Button:
text: "Gastos"
font_size: "12sp"
on_release: gerenciador_telas.current = "gastos"
Button:
text: "Fixos"
font_size: "12sp"
on_release: gerenciador_telas.current = "assinaturas"
Button:
text: "Parcelas"
font_size: "12sp"
on_release: gerenciador_telas.current = "parcelas"
<TelaResumo>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
Label:
text: "Resumo"
font_size: "24sp"
bold: True
halign: "left"
valign: "middle"
text_size: self.size
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
Label:
text: "Aqui será exibido o resumo geral do aplicativo."
font_size: "16sp"
size_hint_y: None
height: dp(40)
<TelaRenda>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
spacing: dp(8)
Label:
text: "Rendas"
font_size: "24sp"
bold: True
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "+"
font_size: "24sp"
size_hint_x: None
width: dp(48)
on_release: app.abrir_cadastro(root.name)
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
Label:
text: "Aqui serão exibidas as rendas cadastradas."
font_size: "16sp"
size_hint_y: None
height: dp(40)
<TelaGastos>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
spacing: dp(8)
Label:
text: "Gastos"
font_size: "24sp"
bold: True
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "+"
font_size: "24sp"
size_hint_x: None
width: dp(48)
on_release: app.abrir_cadastro(root.name)
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
Label:
text: "Aqui serão exibidos os gastos comuns."
font_size: "16sp"
size_hint_y: None
height: dp(40)
<TelaAssinaturas>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
spacing: dp(8)
Label:
text: "Assinaturas"
font_size: "24sp"
bold: True
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "+"
font_size: "24sp"
size_hint_x: None
width: dp(48)
on_release: app.abrir_cadastro(root.name)
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
Label:
text: "Aqui serão exibidas as assinaturas e contas fixas."
font_size: "16sp"
size_hint_y: None
height: dp(40)
<TelaParcelas>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
spacing: dp(8)
Label:
text: "Parcelas"
font_size: "24sp"
bold: True
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "+"
font_size: "24sp"
size_hint_x: None
width: dp(48)
on_release: app.abrir_cadastro(root.name)
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
Label:
text: "Aqui serão exibidas as compras parceladas."
font_size: "16sp"
size_hint_y: None
height: dp(40)
Abaixo segue a classe principal atualizada:
class FinancyApp(App):
def build(self):
caminho_banco = f"{self.user_data_dir}/financy.db"
self.db = BancoDados(caminho_banco)
return LayoutPrincipal()
def abrir_cadastro(self, tela_atual):
if tela_atual == "renda":
print("Abrir cadastro de renda")
elif tela_atual == "gastos":
print("Abrir cadastro de gasto")
elif tela_atual == "assinaturas":
print("Abrir cadastro de assinatura")
elif tela_atual == "parcelas":
print("Abrir cadastro de parcela")
elif tela_atual == "resumo":
print("Tela resumo não possui cadastro direto")
def on_stop(self):
self.db.fechar()
Adicionando cards
Vamos criar um widget de card reutilizável e usar esse mesmo card nas telas de renda, gastos, assinaturas e parcelas. No main.py, adicione uma classe para o card:
from kivy.properties import StringProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import Screen
class CardInfo(BoxLayout):
titulo = StringProperty("")
valor = StringProperty("")
detalhe = StringProperty("")
Agora, no financy.kv, crie o visual do card:
<CardInfo>:
orientation: "vertical"
size_hint_y: None
height: dp(115)
padding: dp(12)
spacing: dp(6)
canvas.before:
Color:
rgba: 1, 1, 1, 1
RoundedRectangle:
pos: self.pos
size: self.size
radius: [dp(12)]
Label:
text: root.titulo
font_size: "18sp"
bold: True
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
size_hint_y: None
height: dp(28)
Label:
text: root.valor
font_size: "16sp"
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
size_hint_y: None
height: dp(26)
Label:
text: root.detalhe
font_size: "14sp"
color: 0.35, 0.35, 0.35, 1
halign: "left"
valign: "middle"
text_size: self.size
size_hint_y: None
height: dp(24)
Segue o financy.kv completo.
#:import dp kivy.metrics.dp
<CardInfo>:
orientation: "vertical"
size_hint_y: None
height: dp(115)
padding: dp(12)
spacing: dp(6)
canvas.before:
Color:
rgba: 1, 1, 1, 1
RoundedRectangle:
pos: self.pos
size: self.size
radius: [dp(12)]
Label:
text: root.titulo
font_size: "18sp"
bold: True
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
size_hint_y: None
height: dp(28)
Label:
text: root.valor
font_size: "16sp"
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
size_hint_y: None
height: dp(26)
Label:
text: root.detalhe
font_size: "14sp"
color: 0.35, 0.35, 0.35, 1
halign: "left"
valign: "middle"
text_size: self.size
size_hint_y: None
height: dp(24)
<LayoutPrincipal>:
orientation: "vertical"
canvas.before:
Color:
rgba: 0.95, 0.95, 0.95, 1
Rectangle:
pos: self.pos
size: self.size
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(12)
canvas.before:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
Label:
text: "Financy"
font_size: "22sp"
bold: True
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
ScreenManager:
id: gerenciador_telas
TelaResumo:
name: "resumo"
TelaRenda:
name: "renda"
TelaGastos:
name: "gastos"
TelaAssinaturas:
name: "assinaturas"
TelaParcelas:
name: "parcelas"
BoxLayout:
size_hint_y: None
height: dp(70)
spacing: dp(4)
padding: dp(4)
canvas.before:
Color:
rgba: 1, 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
Button:
text: "Resumo"
font_size: "12sp"
on_release: gerenciador_telas.current = "resumo"
Button:
text: "Renda"
font_size: "12sp"
on_release: gerenciador_telas.current = "renda"
Button:
text: "Gastos"
font_size: "12sp"
on_release: gerenciador_telas.current = "gastos"
Button:
text: "Fixos"
font_size: "12sp"
on_release: gerenciador_telas.current = "assinaturas"
Button:
text: "Parcelas"
font_size: "12sp"
on_release: gerenciador_telas.current = "parcelas"
<TelaResumo>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
Label:
text: "Resumo"
font_size: "24sp"
bold: True
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
id: lista_resumo
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
<TelaRenda>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
spacing: dp(8)
Label:
text: "Rendas"
font_size: "24sp"
bold: True
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "+"
font_size: "24sp"
size_hint_x: None
width: dp(48)
on_release: app.abrir_cadastro(root.name)
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
id: lista_rendas
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
<TelaGastos>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
spacing: dp(8)
Label:
text: "Gastos"
font_size: "24sp"
bold: True
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "+"
font_size: "24sp"
size_hint_x: None
width: dp(48)
on_release: app.abrir_cadastro(root.name)
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
id: lista_gastos
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
<TelaAssinaturas>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
spacing: dp(8)
Label:
text: "Assinaturas"
font_size: "24sp"
bold: True
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "+"
font_size: "24sp"
size_hint_x: None
width: dp(48)
on_release: app.abrir_cadastro(root.name)
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
id: lista_assinaturas
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
<TelaParcelas>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
spacing: dp(8)
Label:
text: "Parcelas"
font_size: "24sp"
bold: True
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
Button:
text: "+"
font_size: "24sp"
size_hint_x: None
width: dp(48)
on_release: app.abrir_cadastro(root.name)
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
id: lista_parcelas
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
Agora, precisamos fazer o Python preencher esses cards com dados do banco. Vamos começar pela TelaRenda, porque é a base do app.
TelaRenda
A ideia é ela carregar as rendas no on_pre_enter(), limpar a lista e criar um CardInfo para cada renda vinda do SQLite.
from kivy.app import App
from kivy.properties import StringProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import Screen
from db import BancoDados
def formatar_reais(valor_centavos):
valor = valor_centavos / 100
return (
f"R$ {valor:,.2f}"
.replace(",", "X")
.replace(".", ",")
.replace("X", ".")
)
class LayoutPrincipal(BoxLayout):
pass
class CardInfo(BoxLayout):
titulo = StringProperty("")
valor = StringProperty("")
detalhe = StringProperty("")
class TelaResumo(Screen):
pass
class TelaRenda(Screen):
def on_pre_enter(self):
self.carregar_rendas()
def carregar_rendas(self):
app = App.get_running_app()
self.ids.lista_rendas.clear_widgets()
rendas = app.db.listar("rendas")
if not rendas:
self.ids.lista_rendas.add_widget(
CardInfo(
titulo="Nenhuma renda cadastrada",
valor="",
detalhe="Toque no botão + para cadastrar uma renda."
)
)
return
for renda in rendas:
recorrente = "sim" if renda["recorrente"] else "não"
status = "ativa" if renda["ativa"] else "inativa"
detalhes = [
f"Mês: {renda['mes_referencia']}",
f"Recorrente: {recorrente}",
f"Status: {status}"
]
self.ids.lista_rendas.add_widget(
CardInfo(
titulo=renda["nome"],
valor=formatar_reais(renda["valor_centavos"]),
detalhe=" | ".join(detalhes)
)
)
class TelaGastos(Screen):
pass
class TelaAssinaturas(Screen):
pass
class TelaParcelas(Screen):
pass
class FinancyApp(App):
def build(self):
caminho_banco = f"{self.user_data_dir}/financy.db"
self.db = BancoDados(caminho_banco)
return LayoutPrincipal()
def abrir_cadastro(self, tela_atual):
if tela_atual == "renda":
print("Abrir cadastro de renda")
elif tela_atual == "gastos":
print("Abrir cadastro de gasto")
elif tela_atual == "assinaturas":
print("Abrir cadastro de assinatura")
elif tela_atual == "parcelas":
print("Abrir cadastro de parcela")
else:
print("Essa tela não possui cadastro direto.")
def on_stop(self):
self.db.fechar()
FinancyApp().run()
Isso já fica muito bom, mas agora precisamos criar uma tela para quando formos cadastrar os dados.
Telas de cadastro
Agora precisamos criar telas de cadastro. Elas ficam dentro do mesmo ScreenManager, mas são acessadas pelo botão +. Hoje o botão + chama app.abrir_cadastro(root.name), e no main.py esse método ainda só faz print(). O próximo passo é trocar esse print() por navegação para uma tela de cadastro.
No .kv, vamos adicionar uma nova tela ao ScreenManager:
TelaCadastroRenda:
name: "cadastro_renda"
E no main.py, criar a classe:
class TelaCadastroRenda(Screen):
pass
Depois, o método abrir_cadastro() muda para algo assim:
def abrir_cadastro(self, tela_atual):
if tela_atual == "renda":
self.root.ids.gerenciador_telas.current = "cadastro_renda"
elif tela_atual == "gastos":
print("Abrir cadastro de gasto")
elif tela_atual == "assinaturas":
print("Abrir cadastro de assinatura")
elif tela_atual == "parcelas":
print("Abrir cadastro de parcela")
else:
print("Essa tela não possui cadastro direto.")
Vamos criar a tela de cadastro de renda como uma tela separada no ScreenManager. Ela vai pedir os dados básicos da renda, salvar no SQLite e voltar para a tela de rendas. Essa tela deve solicitar:
- nome da renda
- valor da renda
- se é recorrente ou não
- dia de recebimento (Usado para renda recorrente)
- data de recebimento (Usado para renda não recorrente.)
- observação
Adicione a linha abaixo aos imports:
from decimal import Decimal, InvalidOperation
Crie a classe TelaCadastroRenda:
class TelaCadastroRenda(Screen):
def converter_reais_para_centavos(self, texto):
texto = texto.strip()
texto = texto.replace("R$", "")
texto = texto.replace(" ", "")
texto = texto.replace(".", "")
texto = texto.replace(",", ".")
try:
valor = Decimal(texto)
except InvalidOperation:
raise ValueError("Informe um valor válido.")
if valor < 0:
raise ValueError("O valor não pode ser negativo.")
return int(valor * 100)
def validar_mes_referencia(self, texto):
texto = texto.strip()
try:
datetime.strptime(texto, "%Y-%m")
except ValueError:
raise ValueError("Informe o mês de referência no formato AAAA-MM.")
return texto
def salvar_renda(self):
app = App.get_running_app()
nome = self.ids.input_nome.text.strip()
valor = self.ids.input_valor.text.strip()
mes_referencia = self.ids.input_mes_referencia.text.strip()
recorrente = self.ids.input_recorrente.active
observacao = self.ids.input_observacao.text.strip()
if not nome:
self.ids.mensagem.text = "Informe o nome da renda."
return
if not valor:
self.ids.mensagem.text = "Informe o valor da renda."
return
if not mes_referencia:
self.ids.mensagem.text = "Informe o mês de referência."
return
try:
valor_centavos = self.converter_reais_para_centavos(valor)
mes_referencia = self.validar_mes_referencia(mes_referencia)
except ValueError as erro:
self.ids.mensagem.text = str(erro)
return
if not observacao:
observacao = None
app.db.adicionar_renda(
nome=nome,
valor_centavos=valor_centavos,
mes_referencia=mes_referencia,
recorrente=recorrente,
ativa=True,
observacao=observacao
)
self.limpar_campos()
app.root.ids.gerenciador_telas.current = "renda"
def limpar_campos(self):
self.ids.input_nome.text = ""
self.ids.input_valor.text = ""
self.ids.input_mes_referencia.text = ""
self.ids.input_recorrente.active = False
self.ids.input_observacao.text = ""
self.ids.mensagem.text = ""
def voltar(self):
self.limpar_campos()
app = App.get_running_app()
app.root.ids.gerenciador_telas.current = "renda"
Crie a tela de cadastro no final do financy.kv:
<TelaCadastroRenda>:
BoxLayout:
orientation: "vertical"
BoxLayout:
size_hint_y: None
height: dp(60)
padding: dp(16), dp(8)
spacing: dp(8)
Button:
text: "<"
font_size: "20sp"
size_hint_x: None
width: dp(48)
on_release: root.voltar()
Label:
text: "Cadastrar Renda"
font_size: "22sp"
bold: True
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
ScrollView:
do_scroll_x: False
do_scroll_y: True
BoxLayout:
orientation: "vertical"
padding: dp(16)
spacing: dp(12)
size_hint_y: None
height: self.minimum_height
Label:
text: "Nome da renda"
color: 0.1, 0.1, 0.1, 1
size_hint_y: None
height: dp(24)
halign: "left"
valign: "middle"
text_size: self.size
TextInput:
id: input_nome
hint_text: "Exemplo: Salário"
multiline: False
size_hint_y: None
height: dp(48)
Label:
text: "Valor"
color: 0.1, 0.1, 0.1, 1
size_hint_y: None
height: dp(24)
halign: "left"
valign: "middle"
text_size: self.size
TextInput:
id: input_valor
hint_text: "Exemplo: 1500,50"
multiline: False
input_type: "number"
size_hint_y: None
height: dp(48)
BoxLayout:
orientation: "horizontal"
size_hint_y: None
height: dp(48)
spacing: dp(8)
Label:
text: "Renda recorrente?"
color: 0.1, 0.1, 0.1, 1
halign: "left"
valign: "middle"
text_size: self.size
CheckBox:
id: input_recorrente
size_hint_x: None
width: dp(48)
Label:
text: "Mês de referência"
color: 0.1, 0.1, 0.1, 1
size_hint_y: None
height: dp(24)
halign: "left"
valign: "middle"
text_size: self.size
TextInput:
id: input_mes_referencia
hint_text: "Exemplo: 2026-05"
multiline: False
size_hint_y: None
height: dp(48)
Label:
text: "Observação"
color: 0.1, 0.1, 0.1, 1
size_hint_y: None
height: dp(24)
halign: "left"
valign: "middle"
text_size: self.size
TextInput:
id: input_observacao
hint_text: "Informação opcional"
size_hint_y: None
height: dp(90)
Label:
id: mensagem
text: ""
color: 0.8, 0.1, 0.1, 1
size_hint_y: None
height: dp(30)
Button:
text: "Salvar renda"
size_hint_y: None
height: dp(50)
on_release: root.salvar_renda()