Skip to content

Commit 50c1c8d

Browse files
authored
Squashed commit of the following: (#12)
commit 989319906069c1cecf7ed5bd8cee454d87ebcd3f Author: Sung Cho <sung.w.cho@pm.me> Date: Sat Oct 18 20:59:51 2025 -0700 download spreadsheet commit b4b833a9a0e8f04daf2896f131f4ea7398702612 Author: Sung Cho <sung.w.cho@pm.me> Date: Sat Oct 18 20:53:50 2025 -0700 get spreadsheet with formulas
1 parent 7949288 commit 50c1c8d

File tree

5 files changed

+188
-8
lines changed

5 files changed

+188
-8
lines changed

pdm.lock

Lines changed: 26 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ description = "Split Bill Web Site"
2828
authors = [
2929
{name = "Sung Cho", email = "sung.w.cho@protonmail.com"},
3030
]
31-
dependencies = ["flask", "pillow", "openai", "pydantic", "click"]
31+
dependencies = ["flask", "pillow", "openai", "pydantic", "click", "openpyxl"]
3232
requires-python = "==3.12.*"
3333
readme = "README.md"
3434
license = {text = "MIT"}

src/bill/calculator.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import csv
2-
from io import StringIO
2+
from io import StringIO, BytesIO
33
from typing import Iterable
44
from bill.person import Person
55
from bill.receipts import Items, Item
6+
from openpyxl import Workbook
67

78

89
class Calculator:
@@ -195,3 +196,68 @@ def get_person_shares(get_share):
195196

196197
output.seek(0)
197198
return output.getvalue()
199+
200+
def get_shares_spreadsheet(self):
201+
"""
202+
Get an Excel spreadsheet (BytesIO) with formulas for calculating shares.
203+
"""
204+
workbook = Workbook()
205+
worksheet = workbook.active
206+
worksheet.title = "Bill Split"
207+
208+
fieldnames = (
209+
["Item", "Receipt"] + [person.name for person in self.persons] + ["Check"]
210+
)
211+
212+
worksheet.append(fieldnames)
213+
current_row = 2
214+
215+
for item in self.items.items:
216+
row_data = [item.name, item.price]
217+
for person in self.persons:
218+
share = self.get_person_share(item, person)
219+
row_data.append(share if share else None)
220+
row_data.append(None)
221+
worksheet.append(row_data)
222+
current_row += 1
223+
224+
subtotal_row = current_row
225+
subtotal = self.items.get_sum()
226+
person_count = len(self.persons)
227+
last_person_col = chr(ord("C") + person_count - 1)
228+
229+
row_data = ["Subtotal", subtotal]
230+
for col_idx in range(person_count):
231+
col_letter = chr(ord("C") + col_idx)
232+
formula = f"=SUM({col_letter}2:{col_letter}{current_row - 1})"
233+
row_data.append(formula)
234+
row_data.append(f"=SUM(C{current_row}:{last_person_col}{current_row})")
235+
worksheet.append(row_data)
236+
current_row += 1
237+
238+
for extra in self.extras.items:
239+
row_data = [extra.name, extra.price]
240+
for col_idx in range(person_count):
241+
col_letter = chr(ord("C") + col_idx)
242+
formula = (
243+
f"=({col_letter}{subtotal_row}/$B${subtotal_row})*$B{current_row}"
244+
)
245+
row_data.append(formula)
246+
row_data.append(f"=SUM(C{current_row}:{last_person_col}{current_row})")
247+
worksheet.append(row_data)
248+
current_row += 1
249+
250+
total_row = current_row
251+
row_data = ["Total"]
252+
row_data.append(f"=SUM(B{subtotal_row}:B{current_row - 1})")
253+
for col_idx in range(person_count):
254+
col_letter = chr(ord("C") + col_idx)
255+
formula = f"=SUM({col_letter}{subtotal_row}:{col_letter}{current_row - 1})"
256+
row_data.append(formula)
257+
row_data.append(f"=SUM(C{total_row}:{last_person_col}{total_row})")
258+
worksheet.append(row_data)
259+
260+
output = BytesIO()
261+
workbook.save(output)
262+
output.seek(0)
263+
return output

src/ui/payments.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,21 +72,21 @@ def payments_page_view():
7272

7373

7474
@payments_page.route("/payments/download", methods=["GET"])
75-
def download_csv():
75+
def download_spreadsheet():
7676
items = get_current_items(session)
7777
extras = get_current_extras(session)
7878
persons = get_current_persons(session)
7979

8080
calculator = Calculator(persons=persons, items=items, extras=extras)
8181

8282
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
83-
filename = f"{timestamp}.csv"
83+
filename = f"{timestamp}.xlsx"
8484

85-
csv_content = calculator.get_shares_csv()
85+
spreadsheet_bytes = calculator.get_shares_spreadsheet()
8686

8787
return Response(
88-
csv_content,
89-
mimetype="text/csv",
88+
spreadsheet_bytes.getvalue(),
89+
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
9090
headers={"Content-Disposition": f"attachment; filename={filename}"},
9191
)
9292

tests/test_calculator.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from bill.person import Person
44
from bill.receipts import Item, Items
55
from tests.utils import EXPECTED_ITEMS
6+
from openpyxl import load_workbook
67

78
SERVICE_CHARGE = "Service Charge"
89
TAX = "Tax"
@@ -225,3 +226,91 @@ def test_person_update_item():
225226

226227
person.update_item(5)
227228
assert person.items == [1, 7, 9], "Item 5 should be removed again"
229+
230+
231+
def test_spreadsheet(calculator, sample_persons):
232+
spreadsheet_bytes = calculator.get_shares_spreadsheet()
233+
234+
workbook = load_workbook(spreadsheet_bytes)
235+
worksheet = workbook.active
236+
237+
header = [cell.value for cell in worksheet[1]]
238+
expected_header = ["Item", "Receipt", "A", "B", "C", "Check"]
239+
assert header == expected_header
240+
241+
assert worksheet["A2"].value == "GL-Domaine Amido Cotes Du Rhone"
242+
assert worksheet["B2"].value == 13.00
243+
assert worksheet["C2"].value == 6.50
244+
assert worksheet["D2"].value is None
245+
assert worksheet["E2"].value == 6.50
246+
247+
assert worksheet["A16"].value == "Shahi tukda"
248+
assert worksheet["B16"].value == 15.00
249+
assert worksheet["C16"].value == 5.00
250+
assert worksheet["D16"].value == 5.00
251+
assert worksheet["E16"].value == 5.00
252+
253+
assert worksheet["A17"].value == "Jus d Manguir"
254+
assert worksheet["B17"].value == 9.00
255+
assert worksheet["C17"].value == 4.50
256+
assert worksheet["D17"].value == 4.50
257+
assert worksheet["E17"].value is None
258+
259+
subtotal_row = 18
260+
assert worksheet[f"A{subtotal_row}"].value == "Subtotal"
261+
subtotal = EXPECTED_ITEMS.get_sum()
262+
assert worksheet[f"B{subtotal_row}"].value == subtotal
263+
264+
assert worksheet[f"C{subtotal_row}"].value == "=SUM(C2:C17)"
265+
assert worksheet[f"D{subtotal_row}"].value == "=SUM(D2:D17)"
266+
assert worksheet[f"E{subtotal_row}"].value == "=SUM(E2:E17)"
267+
assert worksheet[f"F{subtotal_row}"].value == "=SUM(C18:E18)"
268+
269+
service_charge_row = 19
270+
assert worksheet[f"A{service_charge_row}"].value == SERVICE_CHARGE
271+
service_charge = subtotal * SERVICE_CHARGE_RATIO
272+
assert worksheet[f"B{service_charge_row}"].value == service_charge
273+
274+
assert (
275+
worksheet[f"C{service_charge_row}"].value
276+
== f"=(C{subtotal_row}/$B${subtotal_row})*$B{service_charge_row}"
277+
)
278+
assert (
279+
worksheet[f"D{service_charge_row}"].value
280+
== f"=(D{subtotal_row}/$B${subtotal_row})*$B{service_charge_row}"
281+
)
282+
assert (
283+
worksheet[f"E{service_charge_row}"].value
284+
== f"=(E{subtotal_row}/$B${subtotal_row})*$B{service_charge_row}"
285+
)
286+
assert (
287+
worksheet[f"F{service_charge_row}"].value
288+
== f"=SUM(C{service_charge_row}:E{service_charge_row})"
289+
)
290+
291+
tax_row = 20
292+
assert worksheet[f"A{tax_row}"].value == TAX
293+
tax = subtotal * TAX_RATIO
294+
assert worksheet[f"B{tax_row}"].value == tax
295+
296+
assert (
297+
worksheet[f"C{tax_row}"].value
298+
== f"=(C{subtotal_row}/$B${subtotal_row})*$B{tax_row}"
299+
)
300+
assert (
301+
worksheet[f"D{tax_row}"].value
302+
== f"=(D{subtotal_row}/$B${subtotal_row})*$B{tax_row}"
303+
)
304+
assert (
305+
worksheet[f"E{tax_row}"].value
306+
== f"=(E{subtotal_row}/$B${subtotal_row})*$B{tax_row}"
307+
)
308+
assert worksheet[f"F{tax_row}"].value == f"=SUM(C{tax_row}:E{tax_row})"
309+
310+
total_row = 21
311+
assert worksheet[f"A{total_row}"].value == "Total"
312+
assert worksheet[f"B{total_row}"].value == f"=SUM(B{subtotal_row}:B{tax_row})"
313+
assert worksheet[f"C{total_row}"].value == f"=SUM(C{subtotal_row}:C{tax_row})"
314+
assert worksheet[f"D{total_row}"].value == f"=SUM(D{subtotal_row}:D{tax_row})"
315+
assert worksheet[f"E{total_row}"].value == f"=SUM(E{subtotal_row}:E{tax_row})"
316+
assert worksheet[f"F{total_row}"].value == f"=SUM(C{total_row}:E{total_row})"

0 commit comments

Comments
 (0)