-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
301 lines (249 loc) · 10.5 KB
/
main.py
File metadata and controls
301 lines (249 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
from pyscript import fetch, display
from pyscript.web import page
import csv
from urllib.parse import urlparse
from io import StringIO
async def read_google_sheet(url: str) -> list[dict]:
"""
Lee una hoja de cálculo pública de Google Sheets y la convierte en una lista de diccionarios.
Args:
url (str): URL completa de la hoja de Google Sheets compartida públicamente
Returns:
list: Lista de diccionarios donde cada diccionario representa una fila
con pares clave-valor de columna:valor
Raises:
ValueError: Si la URL no es válida o no está compartida públicamente
RequestException: Si hay problemas al acceder a la hoja
"""
try:
# Verificar si es una URL válida
parsed_url = urlparse(url)
if not parsed_url.scheme or not parsed_url.netloc:
raise ValueError("URL inválida")
# Extraer el ID del documento de la URL
if '/d/' in url:
# URL formato: https://docs.google.com/spreadsheets/d/[ID]/edit?gid=0
doc_id = url.split('/d/')[1].split('/')[0]
gid = url.split('gid=')[1] if 'gid=' in url else '0'
else:
raise ValueError("No se pudo extraer el ID del documento de la URL")
# Construir la URL de exportación CSV
csv_url = f"https://docs.google.com/spreadsheets/d/{doc_id}/export?format=csv&gid={gid}"
# Realizar la solicitud HTTP
response = await fetch(csv_url, method="GET").text()
# Usar CSV reader con StringIO para procesar el contenido
csv_file = StringIO(response)
csv_reader = csv.reader(csv_file)
# Obtener los encabezados (primera fila)
headers = next(csv_reader)
headers = [header.strip() for header in headers] # Limpiar espacios en blanco
# Convertir las filas restantes en diccionarios
records = []
for row in csv_reader:
# Crear diccionario para la fila actual
record = {}
for i, value in enumerate(row):
# Intentar convertir a número si es posible
try:
# Intentar convertir a float
float_value = float(value)
# Si es un número entero, convertir a int
if float_value.is_integer():
record[headers[i]] = int(float_value)
else:
record[headers[i]] = float_value
except ValueError:
# Si no se puede convertir a número, dejar como string
record[headers[i]] = value.strip()
records.append(record)
return records
except Exception as e:
raise Exception(f"Error inesperado: {str(e)}")
def parse_participants(participants_str, total_amount, all_participants):
"""
Parsea la cadena de participantes y sus montos.
Retorna un diccionario con los montos asignados a cada participante.
"""
if not participants_str:
# Si no hay participantes especificados, dividir entre todos equitativamente
amount_per_person = total_amount / len(all_participants)
return {p: amount_per_person for p in all_participants}
# Inicializar el diccionario de participantes y montos
participant_amounts = {}
participants_without_amount = []
remaining_amount = total_amount
# Procesar cada participante
for part in participants_str.split(','):
part = part.strip()
if '=' in part:
# Participante con monto específico
name, amount = part.split('=')
name = name.strip()
amount = float(amount.strip())
if name not in all_participants:
raise ValueError(f"Participante inválido: {name}")
participant_amounts[name] = amount
remaining_amount -= amount
else:
# Participante sin monto específico
if part not in all_participants:
raise ValueError(f"Participante inválido: {part}")
participants_without_amount.append(part)
# Verificar que el monto total no exceda el gasto
if remaining_amount < 0:
raise ValueError("La suma de los montos especificados excede el total del gasto")
# Distribuir el monto restante entre los participantes sin monto específico
if participants_without_amount:
amount_per_remaining = remaining_amount / len(participants_without_amount)
for participant in participants_without_amount:
participant_amounts[participant] = amount_per_remaining
return participant_amounts
def balance_transactions(gastos):
# Inicializar el total de gastos por persona
participantes = list(set(gasto["name"] for gasto in gastos))
for gasto in gastos:
if 'participants' in gasto:
for part in gasto['participants'].split(','):
part = part.split('=')[0].strip()
if bool(part) and (part not in participantes):
participantes.append(part)
extra_participants = page["#extra-participants"][0].value
if extra_participants:
for extra in extra_participants.split(','):
if extra not in participantes:
participantes.append(extra.strip())
gastos_por_persona = {persona: 0 for persona in participantes}
# Para cada gasto, calcular cuánto debe pagar cada participante
for gasto in gastos:
try:
# Determinar los montos por participante
participantes_montos = parse_participants(
gasto.get('participants', ''),
gasto['amount'],
participantes
)
# Asignar los gastos a cada participante
for persona, monto in participantes_montos.items():
gastos_por_persona[persona] += monto
except Exception as e:
raise ValueError(f"Error en gasto '{gasto['item']}': {str(e)}")
# Calcular cuánto ha pagado cada participante en total
pagos_realizados = {persona: 0 for persona in participantes}
for gasto in gastos:
pagos_realizados[gasto['name']] += gasto['amount']
# Calcular el balance de cada participante
balances = {persona: pagos_realizados[persona] - gastos_por_persona[persona]
for persona in participantes}
# Separar participantes en acreedores y deudores
acreedores = [(persona, balance) for persona, balance in balances.items() if balance > 0]
deudores = [(persona, -balance) for persona, balance in balances.items() if balance < 0]
# Minimizar transferencias
transacciones = []
i, j = 0, 0
while i < len(deudores) and j < len(acreedores):
deudor, deuda = deudores[i]
acreedor, credito = acreedores[j]
pago = min(deuda, credito)
if pago >= 0.01: # Solo agregar transacciones significativas
transacciones.append(f"{deudor} debe pagarle a {acreedor} ${pago:.2f}")
deudores[i] = (deudor, deuda - pago)
acreedores[j] = (acreedor, credito - pago)
if deudores[i][1] < 0.01:
i += 1
if acreedores[j][1] < 0.01:
j += 1
output = {
"gastos": gastos,
"gastos_por_persona": gastos_por_persona,
"pagos_realizados": pagos_realizados,
"balances": balances,
"transacciones": transacciones
}
return output
def display_summary(gastos, gastos_por_persona, pagos_realizados, balances, transacciones):
results_div = page["#results"][0]
innerHTML = "<h2>Resultados</h2>"
participantes = list(gastos_por_persona.keys())
display("\nGastos realizados:", target="results")
# Inicio de la tabla HTML con estilos
html = """
<table>
<thead>
<tr>
<th>Pagado por</th>
<th>Concepto</th>
<th>Monto Total</th>
"""
# Agregar encabezados para cada participante
for participante in participantes:
html += f"<th>{participante}</th>"
html += "</tr></thead><tbody>"
# Variables para calcular totales
totales = {p: 0.0 for p in participantes}
total_general = 0.0
# Agregar filas de gastos
for gasto in gastos:
participantes_montos = parse_participants(
gasto.get('participants', ''),
gasto['amount'],
participantes
)
# Actualizar totales
total_general += gasto['amount']
for p, m in participantes_montos.items():
totales[p] += m
# Agregar fila de gasto
html += f"""
<tr>
<td>{gasto['name']}</td>
<td class="concept">{gasto['item']}</td>
<td>${gasto['amount']:,.2f}</td>
"""
# Agregar columnas de montos por participante
for participante in participantes:
monto = participantes_montos.get(participante, 0)
html += f"<td>${monto:.2f}</td>"
html += "</tr>"
# Agregar fila de totales
html += f"""
<tr class="total-row">
<td colspan="2">Total Gastado</td>
<td>${total_general:,.2f}</td>
"""
for participante in participantes:
html += f"<td>${totales[participante]:.2f}</td>"
html += "</tr>"
# Agregar fila de pagos realizados
html += f"""
<tr class="total-row">
<td colspan="2">Total Pagado</td>
<td>${sum(pagos_realizados.values()):,.2f}</td>
"""
for participante in participantes:
html += f"<td>${pagos_realizados[participante]:.2f}</td>"
html += "</tr>"
# Agregar fila de balance final
html += f"""
<tr class="total-row">
<td colspan="2">Balance Final</td>
<td>${sum(balances.values()):,.2f}</td>
"""
for participante in participantes:
balance = balances[participante]
# Agregar color según el balance sea positivo o negativo
color = "#2e7d32" if balance >= 0 else "#c62828"
html += f'<td style="color: {color}">${balance:.2f}</td>'
html += "</tr></tbody></table>"
innerHTML += html
results_div.innerHTML = innerHTML
display("\nPagos pendientes:", target="results")
for transaccion in transacciones:
display(f"- {transaccion}", target="results")
async def calculate_from_url(url):
url = page["#sheet-url"][0].value
if not url:
display("Por favor, ingresa una URL válida", target="results")
return None
gastos = await read_google_sheet(url)
balance_data = balance_transactions(gastos)
display_summary(**balance_data)