Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timesheets rendered and displayed for invoices #186

Merged
merged 10 commits into from
Feb 1, 2023
2 changes: 1 addition & 1 deletion app/auth/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def get_user_if_exists(self) -> IntentResult[Optional[User]]:
"""
result = self._data_source.get_user_()
if not result.was_intent_successful:
result.error_msg = "Checking auth status failed! Please restart the app"
result.error_msg = "No user data found."
result.log_message_if_any()
return result

Expand Down
10 changes: 7 additions & 3 deletions app/core/database_storage_impl.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from typing import Callable

import re
from pathlib import Path
from loguru import logger
from typing import Callable
import demo

import sqlmodel
from loguru import logger

from tuttle import demo

from .abstractions import DatabaseStorage


Expand Down
26 changes: 25 additions & 1 deletion app/invoicing/data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from core.abstractions import SQLModelDataSourceMixin
from core.intent_result import IntentResult

from tuttle.model import Invoice, Project
from tuttle.model import Invoice, Project, Timesheet


class InvoicingDataSource(SQLModelDataSourceMixin):
Expand Down Expand Up @@ -74,6 +74,10 @@ def save_invoice(
"""Creates or updates an invoice with given invoice and project info"""
self.store(invoice)

def save_timesheet(self, timesheet: Timesheet):
"""Creates or updates a timesheet"""
self.store(timesheet)

def get_last_invoice(self) -> IntentResult[Invoice]:
"""Get the last invoice.

Expand All @@ -100,3 +104,23 @@ def get_last_invoice(self) -> IntentResult[Invoice]:
log_message=f"Exception raised @InvoicingDataSource.get_last_invoice_number {e.__class__.__name__}",
exception=e,
)

def get_timesheet_for_invoice(self, invoice: Invoice) -> Timesheet:
"""Get the timesheet associated with an invoice

Args:
invoice (Invoice): the invoice to get the timesheet for

Returns:
Optional[Timesheet]: the timesheet associated with the invoice
"""
if not len(invoice.timesheets) > 0:
raise ValueError(
f"invoice {invoice.id} has no timesheets associated with it"
)
if len(invoice.timesheets) > 1:
raise ValueError(
f"invoice {invoice.id} has more than one timesheet associated with it: {invoice.timesheets}"
)
timesheet = invoice.timesheets[0]
return timesheet
60 changes: 52 additions & 8 deletions app/invoicing/intent.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ def __init__(self, client_storage: ClientStorage):
self._user_data_source = UserDataSource()
self._auth_intent = AuthIntent()

def get_user(self) -> Optional[User]:
"""Get the current user."""
return self._auth_intent.get_user_if_exists()
def get_user(self) -> IntentResult[User]:
user = self._user_data_source.get_user()
return IntentResult(was_intent_successful=True, data=user)

def get_active_projects_as_map(self) -> Mapping[int, Project]:
return self._projects_intent.get_active_projects_as_map()
Expand Down Expand Up @@ -95,11 +95,13 @@ def create_invoice(
render: bool = True,
) -> IntentResult[Invoice]:
"""Create a new invoice from time tracking data."""

logger.info(f"⚙️ Creating invoice for {project.title}...")
user = self._user_data_source.get_user()
try:
# get the time tracking data
timetracking_data = self._timetracking_data_source.get_data_frame()
timesheet: Timesheet = timetracking.create_timesheet(
# generate timesheet
timesheet: Timesheet = timetracking.generate_timesheet(
timetracking_data,
project,
from_date,
Expand All @@ -116,10 +118,24 @@ def create_invoice(
)

if render:
# TODO: render timesheet
# render timesheet
try:
logger.info(f"⚙️ Rendering timesheet for {project.title}...")
rendering.render_timesheet(
user=user,
timesheet=timesheet,
out_dir=Path.home() / ".tuttle" / "Timesheets",
only_final=True,
)
logger.info(f"✅ rendered timesheet for {project.title}")
except Exception as ex:
logger.error(
f"❌ Error rendering timesheet for {project.title}: {ex}"
)
logger.exception(ex)
# render invoice
try:
user = self._user_data_source.get_user()
logger.info(f"⚙️ Rendering invoice for {project.title}...")
rendering.render_invoice(
user=user,
invoice=invoice,
Expand All @@ -130,7 +146,12 @@ def create_invoice(
except Exception as ex:
logger.error(f"❌ Error rendering invoice for {project.title}: {ex}")
logger.exception(ex)
# save invoice

# save invoice and timesheet
timesheet.invoice = invoice
assert timesheet.invoice is not None
assert len(invoice.timesheets) == 1
# self._invoicing_data_source.save_timesheet(timesheet)
self._invoicing_data_source.save_invoice(invoice)
return IntentResult(
was_intent_successful=True,
Expand Down Expand Up @@ -319,6 +340,29 @@ def view_invoice(self, invoice: Invoice) -> IntentResult[None]:
error_msg=error_message,
)

def view_timesheet_for_invoice(self, invoice: Invoice) -> IntentResult[None]:
"""Attempts to open the timesheet for the invoice in the default pdf viewer"""
try:
timesheet = self._invoicing_data_source.get_timesheet_for_invoice(invoice)
timesheet_path = (
Path().home() / ".tuttle" / "Timesheets" / f"{timesheet.prefix}.pdf"
)
preview_pdf(timesheet_path)
return IntentResult(was_intent_successful=True)
except ValueError as ve:
logger.error(f"❌ Error getting timesheet for invoice: {ve}")
logger.exception(ve)
return IntentResult(was_intent_successful=False, error_msg=str(ve))
except Exception as ex:
# display the execption name in the error message
error_message = f"❌ Failed to open the timesheet: {ex.__class__.__name__}"
logger.error(error_message)
logger.exception(ex)
return IntentResult(
was_intent_successful=False,
error_msg=error_message,
)

def generate_invoice_number(
self,
invoice_date: Optional[date] = None,
Expand Down
14 changes: 14 additions & 0 deletions app/invoicing/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ def refresh_invoices(self):
on_delete_clicked=self.on_delete_invoice_clicked,
on_mail_invoice=self.on_mail_invoice,
on_view_invoice=self.on_view_invoice,
on_view_timesheet=self.on_view_timesheet,
toggle_paid_status=self.toggle_paid_status,
toggle_cancelled_status=self.toggle_cancelled_status,
toggle_sent_status=self.toggle_sent_status,
Expand All @@ -241,6 +242,12 @@ def on_view_invoice(self, invoice: Invoice):
if not result.was_intent_successful:
self.show_snack(result.error_msg, is_error=True)

def on_view_timesheet(self, invoice: Invoice):
"""Called when the user clicks view in the context menu of an invoice"""
result = self.intent.view_timesheet_for_invoice(invoice)
if not result.was_intent_successful:
self.show_snack(result.error_msg, is_error=True)

def on_delete_invoice_clicked(self, invoice: Invoice):
"""Called when the user clicks delete in the context menu of an invoice"""
if self.editor is not None:
Expand Down Expand Up @@ -425,6 +432,7 @@ def __init__(
on_delete_clicked,
on_mail_invoice,
on_view_invoice,
on_view_timesheet,
toggle_paid_status,
toggle_sent_status,
toggle_cancelled_status,
Expand All @@ -433,6 +441,7 @@ def __init__(
self.invoice = invoice
self.on_delete_clicked = on_delete_clicked
self.on_view_invoice = on_view_invoice
self.on_view_timesheet = on_view_timesheet
self.on_mail_invoice = on_mail_invoice
self.toggle_paid_status = toggle_paid_status
self.toggle_sent_status = toggle_sent_status
Expand Down Expand Up @@ -504,6 +513,11 @@ def build(self):
txt="View",
on_click=lambda e: self.on_view_invoice(self.invoice),
),
views.TPopUpMenuItem(
icon=icons.VISIBILITY_OUTLINED,
txt="View Timesheet ",
on_click=lambda e: self.on_view_timesheet(self.invoice),
),
views.TPopUpMenuItem(
icon=icons.OUTGOING_MAIL,
txt="Send",
Expand Down
15 changes: 15 additions & 0 deletions tuttle/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pathlib import Path
import io
import re
import calendar

from loguru import logger
import ics
Expand Down Expand Up @@ -180,3 +181,17 @@ class GoogleCalendar(CloudCalendar):

def to_data(self) -> DataFrame:
raise NotImplementedError("TODO")


def get_month_start_end(month_str):
# Parse the string into a datetime object
dt = datetime.datetime.strptime(month_str, "%B %Y")

# Get the date information from the datetime object
year, month = dt.date().year, dt.date().month

# Get the start and end dates of the month
start_date = datetime.date(year, month, 1)
end_date = datetime.date(year, month, calendar.monthrange(year, month)[1])

return start_date, end_date
Loading