Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ description = "Split Bill Web Site"
authors = [
{name = "Sung Cho", email = "sung.w.cho@protonmail.com"},
]
dependencies = ["flask", "pillow", "openai", "pydantic", "click"]
dependencies = ["flask", "pillow", "openai", "pydantic", "click", "openpyxl"]
requires-python = "==3.12.*"
readme = "README.md"
license = {text = "MIT"}
Expand Down
68 changes: 67 additions & 1 deletion src/bill/calculator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import csv
from io import StringIO
from io import StringIO, BytesIO
from typing import Iterable
from bill.person import Person
from bill.receipts import Items, Item
from openpyxl import Workbook


class Calculator:
Expand Down Expand Up @@ -195,3 +196,68 @@ def get_person_shares(get_share):

output.seek(0)
return output.getvalue()

def get_shares_spreadsheet(self):
"""
Get an Excel spreadsheet (BytesIO) with formulas for calculating shares.
"""
workbook = Workbook()
worksheet = workbook.active
worksheet.title = "Bill Split"

fieldnames = (
["Item", "Receipt"] + [person.name for person in self.persons] + ["Check"]
)

worksheet.append(fieldnames)
current_row = 2

for item in self.items.items:
row_data = [item.name, item.price]
for person in self.persons:
share = self.get_person_share(item, person)
row_data.append(share if share else None)
row_data.append(None)
worksheet.append(row_data)
current_row += 1

subtotal_row = current_row
subtotal = self.items.get_sum()
person_count = len(self.persons)
last_person_col = chr(ord("C") + person_count - 1)

row_data = ["Subtotal", subtotal]
for col_idx in range(person_count):
col_letter = chr(ord("C") + col_idx)
formula = f"=SUM({col_letter}2:{col_letter}{current_row - 1})"
row_data.append(formula)
row_data.append(f"=SUM(C{current_row}:{last_person_col}{current_row})")
worksheet.append(row_data)
current_row += 1

for extra in self.extras.items:
row_data = [extra.name, extra.price]
for col_idx in range(person_count):
col_letter = chr(ord("C") + col_idx)
formula = (
f"=({col_letter}{subtotal_row}/$B${subtotal_row})*$B{current_row}"
)
row_data.append(formula)
row_data.append(f"=SUM(C{current_row}:{last_person_col}{current_row})")
worksheet.append(row_data)
current_row += 1

total_row = current_row
row_data = ["Total"]
row_data.append(f"=SUM(B{subtotal_row}:B{current_row - 1})")
for col_idx in range(person_count):
col_letter = chr(ord("C") + col_idx)
formula = f"=SUM({col_letter}{subtotal_row}:{col_letter}{current_row - 1})"
row_data.append(formula)
row_data.append(f"=SUM(C{total_row}:{last_person_col}{total_row})")
worksheet.append(row_data)

output = BytesIO()
workbook.save(output)
output.seek(0)
return output
10 changes: 5 additions & 5 deletions src/ui/payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,21 @@ def payments_page_view():


@payments_page.route("/payments/download", methods=["GET"])
def download_csv():
def download_spreadsheet():
items = get_current_items(session)
extras = get_current_extras(session)
persons = get_current_persons(session)

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

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

csv_content = calculator.get_shares_csv()
spreadsheet_bytes = calculator.get_shares_spreadsheet()

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

Expand Down
89 changes: 89 additions & 0 deletions tests/test_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from bill.person import Person
from bill.receipts import Item, Items
from tests.utils import EXPECTED_ITEMS
from openpyxl import load_workbook

SERVICE_CHARGE = "Service Charge"
TAX = "Tax"
Expand Down Expand Up @@ -225,3 +226,91 @@ def test_person_update_item():

person.update_item(5)
assert person.items == [1, 7, 9], "Item 5 should be removed again"


def test_spreadsheet(calculator, sample_persons):
spreadsheet_bytes = calculator.get_shares_spreadsheet()

workbook = load_workbook(spreadsheet_bytes)
worksheet = workbook.active

header = [cell.value for cell in worksheet[1]]
expected_header = ["Item", "Receipt", "A", "B", "C", "Check"]
assert header == expected_header

assert worksheet["A2"].value == "GL-Domaine Amido Cotes Du Rhone"
assert worksheet["B2"].value == 13.00
assert worksheet["C2"].value == 6.50
assert worksheet["D2"].value is None
assert worksheet["E2"].value == 6.50

assert worksheet["A16"].value == "Shahi tukda"
assert worksheet["B16"].value == 15.00
assert worksheet["C16"].value == 5.00
assert worksheet["D16"].value == 5.00
assert worksheet["E16"].value == 5.00

assert worksheet["A17"].value == "Jus d Manguir"
assert worksheet["B17"].value == 9.00
assert worksheet["C17"].value == 4.50
assert worksheet["D17"].value == 4.50
assert worksheet["E17"].value is None

subtotal_row = 18
assert worksheet[f"A{subtotal_row}"].value == "Subtotal"
subtotal = EXPECTED_ITEMS.get_sum()
assert worksheet[f"B{subtotal_row}"].value == subtotal

assert worksheet[f"C{subtotal_row}"].value == "=SUM(C2:C17)"
assert worksheet[f"D{subtotal_row}"].value == "=SUM(D2:D17)"
assert worksheet[f"E{subtotal_row}"].value == "=SUM(E2:E17)"
assert worksheet[f"F{subtotal_row}"].value == "=SUM(C18:E18)"

service_charge_row = 19
assert worksheet[f"A{service_charge_row}"].value == SERVICE_CHARGE
service_charge = subtotal * SERVICE_CHARGE_RATIO
assert worksheet[f"B{service_charge_row}"].value == service_charge

assert (
worksheet[f"C{service_charge_row}"].value
== f"=(C{subtotal_row}/$B${subtotal_row})*$B{service_charge_row}"
)
assert (
worksheet[f"D{service_charge_row}"].value
== f"=(D{subtotal_row}/$B${subtotal_row})*$B{service_charge_row}"
)
assert (
worksheet[f"E{service_charge_row}"].value
== f"=(E{subtotal_row}/$B${subtotal_row})*$B{service_charge_row}"
)
assert (
worksheet[f"F{service_charge_row}"].value
== f"=SUM(C{service_charge_row}:E{service_charge_row})"
)

tax_row = 20
assert worksheet[f"A{tax_row}"].value == TAX
tax = subtotal * TAX_RATIO
assert worksheet[f"B{tax_row}"].value == tax

assert (
worksheet[f"C{tax_row}"].value
== f"=(C{subtotal_row}/$B${subtotal_row})*$B{tax_row}"
)
assert (
worksheet[f"D{tax_row}"].value
== f"=(D{subtotal_row}/$B${subtotal_row})*$B{tax_row}"
)
assert (
worksheet[f"E{tax_row}"].value
== f"=(E{subtotal_row}/$B${subtotal_row})*$B{tax_row}"
)
assert worksheet[f"F{tax_row}"].value == f"=SUM(C{tax_row}:E{tax_row})"

total_row = 21
assert worksheet[f"A{total_row}"].value == "Total"
assert worksheet[f"B{total_row}"].value == f"=SUM(B{subtotal_row}:B{tax_row})"
assert worksheet[f"C{total_row}"].value == f"=SUM(C{subtotal_row}:C{tax_row})"
assert worksheet[f"D{total_row}"].value == f"=SUM(D{subtotal_row}:D{tax_row})"
assert worksheet[f"E{total_row}"].value == f"=SUM(E{subtotal_row}:E{tax_row})"
assert worksheet[f"F{total_row}"].value == f"=SUM(C{total_row}:E{total_row})"