Skip to main content

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:

main.py
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()

financy.kv
<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.


db.py
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:

test_db.py
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

db.py
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

db.py
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

db.py
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

db.py
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

db.py
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

db.py
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

db.py
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

db.py
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

db.py
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

db.py
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

db.py
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

db.py
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

db.py
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.

db.py
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.

db.py
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.


O main.py foi atualizado com as novas mudanças

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:

db.py
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:

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.

db.py
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

db.py
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

db.py
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

db.py
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.

db.py
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.

db.py
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


db.py
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


financy.kv
#: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.

financy.kv
<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:

financy.kv
#: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:

main.py
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:

main.py
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:

financy.kv
<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.

financy.kv
#: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.

main.py
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:

main.py
from decimal import Decimal, InvalidOperation

Crie a classe TelaCadastroRenda:

main.py
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:

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