Upload 4 files
Browse files- .streamlit/config.toml +6 -0
- finanzas.db +0 -0
- main.py +308 -0
- requirements.txt +5 -0
.streamlit/config.toml
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[theme]
|
2 |
+
primaryColor="#53ba83"
|
3 |
+
backgroundColor="#000020"
|
4 |
+
secondaryBackgroundColor="#2f2c79"
|
5 |
+
textColor="#ffffff"
|
6 |
+
font="serif"
|
finanzas.db
ADDED
Binary file (24.6 kB). View file
|
|
main.py
ADDED
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
from sqlalchemy import create_engine, Column, Integer, String, Float, ForeignKey, Enum
|
3 |
+
from sqlalchemy.orm import sessionmaker, relationship, declarative_base
|
4 |
+
from datetime import datetime
|
5 |
+
import pytz
|
6 |
+
import os
|
7 |
+
import requests
|
8 |
+
from dotenv import load_dotenv
|
9 |
+
from pydantic import BaseModel
|
10 |
+
from typing import List
|
11 |
+
|
12 |
+
st.set_page_config(
|
13 |
+
page_title="Gestión de Ingresos y Gastos",
|
14 |
+
layout="centered",
|
15 |
+
initial_sidebar_state="collapsed",
|
16 |
+
page_icon="💰",
|
17 |
+
)
|
18 |
+
|
19 |
+
# Cargar variables de entorno
|
20 |
+
load_dotenv()
|
21 |
+
|
22 |
+
# URL de la API de Google Apps Script
|
23 |
+
GOOGLE_SHEET_API_URL = os.getenv("GOOGLE_SHEET_API_URL")
|
24 |
+
|
25 |
+
# Conexión con la base de datos SQLite (Finanzas.db)
|
26 |
+
DATABASE_URL = "sqlite:///finanzas.db"
|
27 |
+
engine = create_engine(DATABASE_URL)
|
28 |
+
|
29 |
+
# Crear una clase base para el ORM
|
30 |
+
Base = declarative_base()
|
31 |
+
|
32 |
+
# Definición de modelos ORM
|
33 |
+
|
34 |
+
|
35 |
+
class Usuario(Base):
|
36 |
+
__tablename__ = "usuarios"
|
37 |
+
|
38 |
+
id_usuario = Column(Integer, primary_key=True, index=True)
|
39 |
+
nombre = Column(String, nullable=False)
|
40 |
+
|
41 |
+
|
42 |
+
class Categoria(Base):
|
43 |
+
__tablename__ = "categorias"
|
44 |
+
|
45 |
+
id_categoria = Column(Integer, primary_key=True, autoincrement=True)
|
46 |
+
nombre_categoria = Column(String, nullable=False)
|
47 |
+
tipo = Column(Enum("Ingreso", "Gasto", name="tipo_categoria"), nullable=False)
|
48 |
+
|
49 |
+
|
50 |
+
class Subcategoria(Base):
|
51 |
+
__tablename__ = "subcategorias"
|
52 |
+
|
53 |
+
id_subcategoria = Column(Integer, primary_key=True, autoincrement=True)
|
54 |
+
id_categoria = Column(
|
55 |
+
Integer, ForeignKey("categorias.id_categoria"), nullable=False
|
56 |
+
)
|
57 |
+
nombre_subcategoria = Column(String, nullable=False)
|
58 |
+
|
59 |
+
categoria = relationship("Categoria", backref="subcategorias")
|
60 |
+
|
61 |
+
|
62 |
+
class Transaccion(Base):
|
63 |
+
__tablename__ = "transacciones"
|
64 |
+
|
65 |
+
id_transaccion = Column(Integer, primary_key=True, autoincrement=True)
|
66 |
+
fecha = Column(String, nullable=False)
|
67 |
+
tipo = Column(Enum("Ingreso", "Gasto", name="tipo_transaccion"), nullable=False)
|
68 |
+
monto = Column(Float, nullable=False)
|
69 |
+
id_subcategoria = Column(
|
70 |
+
Integer, ForeignKey("subcategorias.id_subcategoria"), nullable=False
|
71 |
+
)
|
72 |
+
descripcion = Column(String)
|
73 |
+
id_usuario = Column(Integer, ForeignKey("usuarios.id_usuario"), nullable=False)
|
74 |
+
|
75 |
+
subcategoria = relationship("Subcategoria", backref="transacciones")
|
76 |
+
usuario = relationship("Usuario", backref="transacciones")
|
77 |
+
|
78 |
+
|
79 |
+
# Función para obtener la fecha y hora precisa en la zona horaria de Perú
|
80 |
+
def get_peru_time():
|
81 |
+
peru_tz = pytz.timezone("America/Lima")
|
82 |
+
return datetime.now(peru_tz).strftime("%Y-%m-%d %H:%M:%S")
|
83 |
+
|
84 |
+
|
85 |
+
# Función para enviar datos a la hoja de Google Sheets
|
86 |
+
def send_to_google_sheet(data: List[BaseModel]):
|
87 |
+
"""
|
88 |
+
Envía los datos a la Google Sheet mediante la API de Google Apps Script.
|
89 |
+
:param data: Lista de registros con toda la información.
|
90 |
+
"""
|
91 |
+
# Asegúrate de convertir los objetos Pydantic en diccionarios
|
92 |
+
data_dicts = [
|
93 |
+
record.model_dump() for record in data
|
94 |
+
] # Convertir cada objeto en un diccionario
|
95 |
+
|
96 |
+
# Enviar los datos como una lista de diccionarios
|
97 |
+
response = requests.post(GOOGLE_SHEET_API_URL, json=data_dicts)
|
98 |
+
|
99 |
+
if response.status_code == 200:
|
100 |
+
print("Datos enviados con éxito a la Google Sheet.")
|
101 |
+
else:
|
102 |
+
print(f"Error al enviar datos: {response.status_code}, {response.text}")
|
103 |
+
|
104 |
+
|
105 |
+
# Crear una sesión
|
106 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
107 |
+
|
108 |
+
|
109 |
+
def insert_transaccion(fecha, tipo, monto, id_subcategoria, descripcion, id_usuario):
|
110 |
+
# Crear una nueva sesión
|
111 |
+
db = SessionLocal()
|
112 |
+
|
113 |
+
try:
|
114 |
+
# Crear la transacción
|
115 |
+
transaccion = Transaccion(
|
116 |
+
fecha=fecha,
|
117 |
+
tipo=tipo,
|
118 |
+
monto=monto,
|
119 |
+
descripcion=descripcion,
|
120 |
+
id_subcategoria=id_subcategoria,
|
121 |
+
id_usuario=id_usuario,
|
122 |
+
)
|
123 |
+
db.add(transaccion)
|
124 |
+
db.commit()
|
125 |
+
|
126 |
+
# Obtener los detalles de la transacción (subcategoría, categoría, etc.)
|
127 |
+
transaccion_db = (
|
128 |
+
db.query(Transaccion)
|
129 |
+
.filter(Transaccion.id_transaccion == transaccion.id_transaccion)
|
130 |
+
.first()
|
131 |
+
)
|
132 |
+
|
133 |
+
if transaccion_db:
|
134 |
+
subcategoria = transaccion_db.subcategoria.nombre_subcategoria
|
135 |
+
categoria = transaccion_db.subcategoria.categoria.nombre_categoria
|
136 |
+
usuario = transaccion_db.usuario.nombre
|
137 |
+
|
138 |
+
# Verificar los valores antes de crear el Pydantic
|
139 |
+
# print(
|
140 |
+
# f"Subcategoría: {subcategoria}, Categoría: {categoria}, Usuario: {usuario}"
|
141 |
+
# )
|
142 |
+
|
143 |
+
# Pydantic model para la transacción
|
144 |
+
transaccion_pydantic = TransaccionPydantic(
|
145 |
+
fecha=fecha,
|
146 |
+
tipo=tipo,
|
147 |
+
monto=monto,
|
148 |
+
descripcion=descripcion,
|
149 |
+
subcategoria=subcategoria,
|
150 |
+
categoria=categoria,
|
151 |
+
usuario=usuario,
|
152 |
+
)
|
153 |
+
# Verificar si el modelo Pydantic tiene datos válidos
|
154 |
+
# print(f"Datos a enviar: {transaccion_pydantic.model_dump()}")
|
155 |
+
|
156 |
+
send_to_google_sheet([transaccion_pydantic])
|
157 |
+
# print(transaccion_pydantic.model_dump())
|
158 |
+
|
159 |
+
finally:
|
160 |
+
db.close()
|
161 |
+
|
162 |
+
|
163 |
+
# Función para obtener los usuarios
|
164 |
+
def get_usuarios():
|
165 |
+
db = SessionLocal()
|
166 |
+
try:
|
167 |
+
return db.query(Usuario).all()
|
168 |
+
finally:
|
169 |
+
db.close()
|
170 |
+
|
171 |
+
|
172 |
+
# Función para obtener las categorías
|
173 |
+
def get_categorias():
|
174 |
+
db = SessionLocal()
|
175 |
+
try:
|
176 |
+
return db.query(Categoria).all()
|
177 |
+
finally:
|
178 |
+
db.close()
|
179 |
+
|
180 |
+
|
181 |
+
# Función para obtener las subcategorías por categoría
|
182 |
+
def get_subcategorias(id_categoria=None):
|
183 |
+
db = SessionLocal()
|
184 |
+
try:
|
185 |
+
if id_categoria:
|
186 |
+
return (
|
187 |
+
db.query(Subcategoria)
|
188 |
+
.filter(Subcategoria.id_categoria == id_categoria)
|
189 |
+
.all()
|
190 |
+
)
|
191 |
+
return db.query(Subcategoria).all()
|
192 |
+
finally:
|
193 |
+
db.close()
|
194 |
+
|
195 |
+
|
196 |
+
# Función para obtener las transacciones completas
|
197 |
+
def get_full_transacciones():
|
198 |
+
db = SessionLocal()
|
199 |
+
try:
|
200 |
+
result = (
|
201 |
+
db.query(Transaccion).join(Subcategoria).join(Categoria).join(Usuario).all()
|
202 |
+
)
|
203 |
+
return [
|
204 |
+
{
|
205 |
+
"id_transaccion": t.id_transaccion,
|
206 |
+
"fecha": t.fecha,
|
207 |
+
"tipo": t.tipo,
|
208 |
+
"monto": t.monto,
|
209 |
+
"descripcion": t.descripcion,
|
210 |
+
"nombre_subcategoria": t.subcategoria.nombre_subcategoria,
|
211 |
+
"nombre_categoria": t.subcategoria.categoria.nombre_categoria,
|
212 |
+
"usuario": t.usuario.nombre,
|
213 |
+
}
|
214 |
+
for t in result
|
215 |
+
]
|
216 |
+
finally:
|
217 |
+
db.close()
|
218 |
+
|
219 |
+
|
220 |
+
# Definición de modelos Pydantic
|
221 |
+
|
222 |
+
|
223 |
+
class TransaccionPydantic(BaseModel):
|
224 |
+
fecha: str
|
225 |
+
tipo: str
|
226 |
+
monto: float
|
227 |
+
descripcion: str
|
228 |
+
subcategoria: str
|
229 |
+
categoria: str
|
230 |
+
usuario: str
|
231 |
+
|
232 |
+
|
233 |
+
class CategoriaPydantic(BaseModel):
|
234 |
+
id_categoria: int
|
235 |
+
nombre_categoria: str
|
236 |
+
tipo: str
|
237 |
+
|
238 |
+
|
239 |
+
class SubcategoriaPydantic(BaseModel):
|
240 |
+
id_subcategoria: int
|
241 |
+
nombre_subcategoria: str
|
242 |
+
|
243 |
+
|
244 |
+
class UsuarioPydantic(BaseModel):
|
245 |
+
id_usuario: int
|
246 |
+
nombre: str
|
247 |
+
|
248 |
+
|
249 |
+
# Interfaz de Streamlit
|
250 |
+
st.title("Gestión de Ingresos y Gastos")
|
251 |
+
|
252 |
+
menu = st.sidebar.selectbox("Menú", ["Registrar Transacción", "Ver Transacciones"])
|
253 |
+
|
254 |
+
if menu == "Registrar Transacción":
|
255 |
+
st.header("Registrar Transacción")
|
256 |
+
|
257 |
+
# Selección de usuario
|
258 |
+
usuarios = get_usuarios()
|
259 |
+
usuario = st.selectbox(
|
260 |
+
"Usuario",
|
261 |
+
[(u.id_usuario, u.nombre) for u in usuarios],
|
262 |
+
format_func=lambda x: x[1],
|
263 |
+
)
|
264 |
+
id_usuario = usuario[0]
|
265 |
+
|
266 |
+
# Selección de tipo (Ingreso o Gasto)
|
267 |
+
tipo = st.selectbox("Tipo", ["Ingreso", "Gasto"])
|
268 |
+
|
269 |
+
# Selección de categoría
|
270 |
+
categorias = [c for c in get_categorias() if c.tipo == tipo]
|
271 |
+
categoria = st.selectbox(
|
272 |
+
"Categoría", categorias, format_func=lambda x: x.nombre_categoria
|
273 |
+
)
|
274 |
+
id_categoria = categoria.id_categoria
|
275 |
+
|
276 |
+
# Selección de subcategoría
|
277 |
+
subcategorias = get_subcategorias(id_categoria)
|
278 |
+
subcategoria = st.selectbox(
|
279 |
+
"Subcategoría", subcategorias, format_func=lambda x: x.nombre_subcategoria
|
280 |
+
)
|
281 |
+
id_subcategoria = subcategoria.id_subcategoria
|
282 |
+
|
283 |
+
# Datos de la transacción
|
284 |
+
monto = st.number_input("Monto", min_value=0.0, step=0.01, format="%.2f")
|
285 |
+
descripcion = st.text_area("Descripción")
|
286 |
+
fecha = get_peru_time() # Obtener la hora y fecha precisa en zona horaria de Perú
|
287 |
+
|
288 |
+
if st.button("Guardar Transacción"):
|
289 |
+
insert_transaccion(
|
290 |
+
fecha,
|
291 |
+
tipo,
|
292 |
+
monto,
|
293 |
+
id_subcategoria,
|
294 |
+
descripcion,
|
295 |
+
id_usuario,
|
296 |
+
)
|
297 |
+
st.success("¡Transacción registrada con éxito!")
|
298 |
+
|
299 |
+
elif menu == "Ver Transacciones":
|
300 |
+
st.header("Ver Transacciones")
|
301 |
+
transacciones = get_full_transacciones()
|
302 |
+
|
303 |
+
# Mostrar en una tabla
|
304 |
+
st.dataframe(
|
305 |
+
transacciones,
|
306 |
+
use_container_width=True,
|
307 |
+
hide_index=True,
|
308 |
+
)
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
pandas
|
2 |
+
python-dotenv
|
3 |
+
sqlalchemy
|
4 |
+
pymysql
|
5 |
+
pydantic
|