Skip to content

Commit a1148ef

Browse files
committed
invoices page: open and preview invoices
1 parent 3c54eec commit a1148ef

File tree

5 files changed

+132
-5
lines changed

5 files changed

+132
-5
lines changed

app/Tuttle.py

+32
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
Contract,
4040
Project,
4141
Client,
42+
Invoice,
4243
)
4344

4445
from tuttle_tests import demo
@@ -276,6 +277,30 @@ def update_content(self):
276277
self.update()
277278

278279

280+
class InvoicesPage(AppPage):
281+
def __init__(
282+
self,
283+
app: App,
284+
):
285+
super().__init__(app)
286+
287+
def update(self):
288+
super().update()
289+
290+
def update_content(self):
291+
super().update_content()
292+
self.main_column.controls.clear()
293+
294+
invoices = self.app.con.query(Invoice)
295+
296+
for invoice in invoices:
297+
self.main_column.controls.append(
298+
# TODO: replace with view class
299+
views.make_invoice_view(invoice, self)
300+
)
301+
self.update()
302+
303+
279304
class InvoicingPage(AppPage):
280305
"""A page for the invoicing workflow."""
281306

@@ -535,6 +560,13 @@ def main(page: Page):
535560
),
536561
InvoicingPage(app),
537562
),
563+
(
564+
NavigationRailDestination(
565+
icon=icons.OUTGOING_MAIL,
566+
label="Invoices",
567+
),
568+
InvoicesPage(app),
569+
),
538570
(
539571
NavigationRailDestination(
540572
icon=icons.SETTINGS,

app/views.py

+46
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
Client,
2828
Contract,
2929
Project,
30+
Invoice,
3031
)
3132

3233

@@ -385,6 +386,51 @@ def make_client_view(
385386
)
386387

387388

389+
def make_invoice_view(
390+
invoice: Invoice,
391+
app_page,
392+
):
393+
return Card(
394+
content=Container(
395+
content=Column(
396+
[
397+
ListTile(
398+
leading=Icon(icons.OUTGOING_MAIL),
399+
title=Text(invoice.number),
400+
# subtitle=Text(client.company),
401+
trailing=PopupMenuButton(
402+
icon=icons.MORE_VERT,
403+
items=[
404+
PopupMenuItem(
405+
icon=icons.VISIBILITY,
406+
text="View",
407+
on_click=lambda _: app_page.app.con.quicklook_invoice(
408+
invoice
409+
),
410+
),
411+
PopupMenuItem(
412+
icon=icons.PREVIEW,
413+
text="Open",
414+
on_click=lambda _: app_page.app.con.open_invoice(
415+
invoice
416+
),
417+
),
418+
PopupMenuItem(
419+
icon=icons.DELETE,
420+
text="Delete",
421+
),
422+
],
423+
),
424+
),
425+
Column([]),
426+
]
427+
),
428+
# width=400,
429+
padding=10,
430+
)
431+
)
432+
433+
388434
def make_card(content):
389435
"""Make a card with the given content."""
390436
return Card(

tuttle/controller.py

+41
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import sys
55
import datetime
66
from typing import Type
7+
import platform
8+
import subprocess
79

810
import pandas
911
import sqlmodel
@@ -257,6 +259,8 @@ def billing(
257259
period_end=period_end,
258260
item_description=project.title,
259261
)
262+
# FIXME: store timesheet
263+
# self.store(timesheet)
260264
logger.info(f"✅ timesheet generated for {project.title}")
261265
try:
262266
rendering.render_timesheet(
@@ -289,3 +293,40 @@ def billing(
289293
except Exception as ex:
290294
logger.error(f"❌ Error rendering invoice for {project.title}: {ex}")
291295
logger.exception(ex)
296+
# finally store invoice
297+
self.store(invoice)
298+
299+
def open_invoice(self, invoice):
300+
"""Open an invoice in the default application for PDF files"""
301+
invoice_file_path = (
302+
self.home
303+
/ self.preferences.invoice_dir
304+
/ Path(invoice.prefix)
305+
/ Path(f"{invoice.prefix}.pdf")
306+
)
307+
if invoice_file_path.exists():
308+
if platform.system() == "Darwin": # macOS
309+
subprocess.call(("open", invoice_file_path))
310+
elif platform.system() == "Windows": # Windows
311+
os.startfile(invoice_file_path)
312+
else: # linux variants
313+
subprocess.call(("xdg-open", invoice_file_path))
314+
315+
else:
316+
logger.error(f"invoice file {invoice_file_path} not found")
317+
318+
def quicklook_invoice(self, invoice):
319+
"""Open an invoice in the preview application for PDF files"""
320+
invoice_file_path = (
321+
self.home
322+
/ self.preferences.invoice_dir
323+
/ Path(invoice.prefix)
324+
/ Path(f"{invoice.prefix}.pdf")
325+
)
326+
if invoice_file_path.exists():
327+
if platform.system() == "Darwin": # macOS
328+
subprocess.call(["qlmanage", "-p", invoice_file_path])
329+
else:
330+
logger.error(f"quicklook not supported on {platform.system()}")
331+
else:
332+
logger.error(f"invoice file {invoice_file_path} not found")

tuttle/model.py

+8
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ class Invoice(SQLModel, table=True):
339339
# payment: Optional["Payment"] = Relationship(back_populates="invoice")
340340
# invoice items
341341
items: List["InvoiceItem"] = Relationship(back_populates="invoice")
342+
rendered: bool = Field(default=False)
342343

343344
#
344345
@property
@@ -375,6 +376,13 @@ def due_date(self):
375376
def client(self):
376377
return self.contract.client
377378

379+
@property
380+
def prefix(self):
381+
"""A string that can be used as the prefix of a file name, or a folder name."""
382+
client_suffix = self.client.name.lower().split()[0]
383+
prefix = f"{self.number}-{client_suffix}"
384+
return prefix
385+
378386

379387
class InvoiceItem(SQLModel, table=True):
380388
id: Optional[int] = Field(default=None, primary_key=True)

tuttle/rendering.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,9 @@ def as_percentage(number):
9393
return html
9494
else:
9595
# write invoice html
96-
client_suffix = invoice.client.name.lower().split()[0]
97-
prefix = f"{invoice.number}-{client_suffix}"
98-
invoice_dir = Path(out_dir) / Path(prefix)
96+
invoice_dir = Path(out_dir) / Path(invoice.prefix)
9997
invoice_dir.mkdir(parents=True, exist_ok=True)
100-
invoice_path = invoice_dir / Path(f"{prefix}.html")
98+
invoice_path = invoice_dir / Path(f"{invoice.prefix}.html")
10199
with open(invoice_path, "w") as invoice_file:
102100
invoice_file.write(html)
103101
# copy stylsheets
@@ -126,8 +124,10 @@ def as_percentage(number):
126124
convert_html_to_pdf(
127125
in_path=str(invoice_path),
128126
css_paths=css_paths,
129-
out_path=invoice_dir / Path(f"{prefix}.pdf"),
127+
out_path=invoice_dir / Path(f"{invoice.prefix}.pdf"),
130128
)
129+
# finally set the rendered flag
130+
invoice.rendered = True
131131

132132

133133
def render_timesheet(

0 commit comments

Comments
 (0)