Feature: Add export functionality for worked hours (CSV, JSON, XLSX)
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -35,3 +35,9 @@ env/
|
||||
# OS specific
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local config and exports
|
||||
config.json
|
||||
tamigo_export_*.csv
|
||||
tamigo_export_*.json
|
||||
tamigo_export_*.xlsx
|
||||
|
||||
@@ -3,3 +3,4 @@ questionary
|
||||
rich
|
||||
python-dotenv
|
||||
pyinstaller
|
||||
openpyxl
|
||||
|
||||
77
tamigo.py
77
tamigo.py
@@ -163,7 +163,7 @@ class TamigoClient:
|
||||
except: pass
|
||||
return all_shifts
|
||||
|
||||
def calculate_checkins(client):
|
||||
def select_date_range():
|
||||
range_choice = questionary.select(
|
||||
"SELECT TEMPORAL RANGE:" if current_theme_name == "fallout" else "Select period:",
|
||||
choices=["Last 365 days", "Last 30 days", "This Month", "This Year", "Custom range..."],
|
||||
@@ -184,7 +184,9 @@ def calculate_checkins(client):
|
||||
if start_date_dt <= end_date_dt: break
|
||||
console.print("[error]ERROR: INVALID CHRONOLOGY.[/error]")
|
||||
except: console.print("[error]ERROR: INVALID DATA FORMAT.[/error]")
|
||||
return start_date_dt, end_date_dt
|
||||
|
||||
def get_worked_days(client, start_date_dt, end_date_dt):
|
||||
with console.status("[highlight]INITIALIZING DATA RETRIEVAL...[/highlight]" if current_theme_name == "fallout" else "Fetching data..."):
|
||||
data = client.get_employee_actual_shifts(start_date_dt, end_date_dt)
|
||||
|
||||
@@ -201,7 +203,14 @@ def calculate_checkins(client):
|
||||
if (hours > 0 or item.get("CheckInTime") or item.get("ActualStartTime") or item.get("StartTime")) and not item.get("IsAbsent", False):
|
||||
if date_str not in work_days: work_days[date_str] = {"hours": hours, "text": item.get("ActualShiftText") or item.get("ActivityName") or "WORKED"}
|
||||
else: work_days[date_str]["hours"] += hours
|
||||
|
||||
return work_days
|
||||
return None
|
||||
|
||||
def calculate_checkins(client):
|
||||
start_date_dt, end_date_dt = select_date_range()
|
||||
work_days = get_worked_days(client, start_date_dt, end_date_dt)
|
||||
|
||||
if work_days is not None:
|
||||
if not work_days:
|
||||
console.print("[warning]WARNING: NO LOG ENTRIES DETECTED.[/warning]")
|
||||
return
|
||||
@@ -220,6 +229,67 @@ def calculate_checkins(client):
|
||||
console.print(Panel(summary_text, title="SUMMARY STATISTICS" if current_theme_name == "fallout" else "Work Statistics", border_style="highlight", expand=False))
|
||||
else: console.print("[error]FATAL ERROR: DATA RETRIEVAL FAILURE.[/error]")
|
||||
|
||||
def export_worked_hours(client):
|
||||
start_date_dt, end_date_dt = select_date_range()
|
||||
work_days = get_worked_days(client, start_date_dt, end_date_dt)
|
||||
|
||||
if work_days is not None:
|
||||
if not work_days:
|
||||
console.print("[warning]WARNING: NO LOG ENTRIES DETECTED.[/warning]")
|
||||
return
|
||||
|
||||
format_choice = questionary.select(
|
||||
"SELECT EXPORT FORMAT:" if current_theme_name == "fallout" else "Select export format:",
|
||||
choices=["CSV", "JSON", "XLSX"],
|
||||
style=theme_data["questionary"]
|
||||
).ask()
|
||||
|
||||
default_filename = f"tamigo_export_{start_date_dt.strftime('%Y%m%d')}_{end_date_dt.strftime('%Y%m%d')}"
|
||||
filename = questionary.text(
|
||||
"ENTER FILENAME:" if current_theme_name == "fallout" else "Enter filename:",
|
||||
default=f"{default_filename}.{format_choice.lower()}",
|
||||
style=theme_data["questionary"]
|
||||
).ask()
|
||||
|
||||
if not filename: return
|
||||
|
||||
try:
|
||||
if format_choice == "CSV":
|
||||
import csv
|
||||
with open(filename, 'w', newline='') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(["Date", "Hours", "Details"])
|
||||
for day in sorted(work_days.keys()):
|
||||
info = work_days[day]
|
||||
writer.writerow([day, f"{info['hours']:.2f}", info['text']])
|
||||
elif format_choice == "XLSX":
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Worked Hours"
|
||||
|
||||
# Header
|
||||
headers = ["Date", "Hours", "Details"]
|
||||
ws.append(headers)
|
||||
for cell in ws[1]:
|
||||
cell.font = Font(bold=True)
|
||||
|
||||
# Data
|
||||
for day in sorted(work_days.keys()):
|
||||
info = work_days[day]
|
||||
ws.append([day, info['hours'], info['text']])
|
||||
|
||||
wb.save(filename)
|
||||
else:
|
||||
with open(filename, 'w') as f:
|
||||
json.dump(work_days, f, indent=4)
|
||||
|
||||
console.print(f"[success]DATA EXPORTED SUCCESSFULLY TO: {filename}[/success]")
|
||||
except Exception as e:
|
||||
console.print(f"[error]ERROR SAVING FILE: {str(e)}[/error]")
|
||||
else: console.print("[error]FATAL ERROR: DATA RETRIEVAL FAILURE.[/error]")
|
||||
|
||||
def main():
|
||||
print_header()
|
||||
client = TamigoClient()
|
||||
@@ -237,11 +307,12 @@ def menu(client):
|
||||
while True:
|
||||
choice = questionary.select(
|
||||
"SELECT ACTION:" if current_theme_name == "fallout" else "What would you like to do?",
|
||||
choices=["Calculate actual work days", "Show profile info", "Switch UI Theme", "Logout and Exit"],
|
||||
choices=["Calculate actual work days", "Export hours worked", "Show profile info", "Switch UI Theme", "Logout and Exit"],
|
||||
style=theme_data["questionary"]
|
||||
).ask()
|
||||
|
||||
if choice == "Calculate actual work days": calculate_checkins(client)
|
||||
elif choice == "Export hours worked": export_worked_hours(client)
|
||||
elif choice == "Show profile info":
|
||||
if client.user_info: console.print_json(data=client.user_info)
|
||||
else: console.print("[error]ERROR: NO PROFILE INFO.[/error]")
|
||||
|
||||
131
test_export.py
Normal file
131
test_export.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import os
|
||||
import csv
|
||||
from tamigo import get_worked_days, TamigoClient
|
||||
|
||||
class TestTamigoExport(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = MagicMock(spec=TamigoClient)
|
||||
self.client.session_token = "fake-token"
|
||||
|
||||
def test_get_worked_days_processing(self):
|
||||
# Mock API response
|
||||
mock_shifts = [
|
||||
{
|
||||
"Date": "/Date(1741651200000)/", # 2025-03-11
|
||||
"ActualShiftHours": 8.5,
|
||||
"ActualShiftText": "Work",
|
||||
"IsAbsent": False
|
||||
},
|
||||
{
|
||||
"StartTime": "2025-03-10T09:00:00Z",
|
||||
"EndTime": "2025-03-10T17:00:00Z",
|
||||
"ActualShiftHours": 0,
|
||||
"IsAbsent": False
|
||||
}
|
||||
]
|
||||
self.client.get_employee_actual_shifts.return_value = mock_shifts
|
||||
|
||||
start_date = datetime(2025, 3, 1)
|
||||
end_date = datetime(2025, 3, 15)
|
||||
|
||||
with patch('tamigo.console'):
|
||||
work_days = get_worked_days(self.client, start_date, end_date)
|
||||
|
||||
self.assertIsNotNone(work_days)
|
||||
self.assertIn("2025-03-11", work_days)
|
||||
self.assertEqual(work_days["2025-03-11"]["hours"], 8.5)
|
||||
self.assertIn("2025-03-10", work_days)
|
||||
self.assertEqual(work_days["2025-03-10"]["hours"], 8.0)
|
||||
|
||||
@patch('tamigo.select_date_range')
|
||||
@patch('tamigo.get_worked_days')
|
||||
@patch('questionary.select')
|
||||
@patch('questionary.text')
|
||||
@patch('tamigo.console')
|
||||
def test_export_worked_hours_csv(self, mock_console, mock_text, mock_select, mock_get_worked, mock_date_range):
|
||||
from tamigo import export_worked_hours
|
||||
|
||||
mock_date_range.return_value = (datetime(2025, 3, 1), datetime(2025, 3, 15))
|
||||
mock_get_worked.return_value = {
|
||||
"2025-03-11": {"hours": 8.5, "text": "Work"},
|
||||
"2025-03-10": {"hours": 8.0, "text": "Work"}
|
||||
}
|
||||
|
||||
mock_select.return_value.ask.return_value = "CSV"
|
||||
mock_text.return_value.ask.return_value = "test_export.csv"
|
||||
|
||||
export_worked_hours(self.client)
|
||||
|
||||
self.assertTrue(os.path.exists("test_export.csv"))
|
||||
with open("test_export.csv", "r") as f:
|
||||
reader = csv.reader(f)
|
||||
rows = list(reader)
|
||||
self.assertEqual(rows[0], ["Date", "Hours", "Details"])
|
||||
# CSV rows might be in different order if dict keys are not sorted, but sorted() was used in code
|
||||
self.assertEqual(rows[1], ["2025-03-10", "8.00", "Work"])
|
||||
self.assertEqual(rows[2], ["2025-03-11", "8.50", "Work"])
|
||||
|
||||
os.remove("test_export.csv")
|
||||
|
||||
@patch('tamigo.select_date_range')
|
||||
@patch('tamigo.get_worked_days')
|
||||
@patch('questionary.select')
|
||||
@patch('questionary.text')
|
||||
@patch('tamigo.console')
|
||||
def test_export_worked_hours_json(self, mock_console, mock_text, mock_select, mock_get_worked, mock_date_range):
|
||||
from tamigo import export_worked_hours
|
||||
|
||||
mock_date_range.return_value = (datetime(2025, 3, 1), datetime(2025, 3, 15))
|
||||
mock_get_worked.return_value = {
|
||||
"2025-03-11": {"hours": 8.5, "text": "Work"},
|
||||
"2025-03-10": {"hours": 8.0, "text": "Work"}
|
||||
}
|
||||
|
||||
mock_select.return_value.ask.return_value = "JSON"
|
||||
mock_text.return_value.ask.return_value = "test_export.json"
|
||||
|
||||
export_worked_hours(self.client)
|
||||
|
||||
self.assertTrue(os.path.exists("test_export.json"))
|
||||
with open("test_export.json", "r") as f:
|
||||
data = json.load(f)
|
||||
self.assertEqual(data["2025-03-11"]["hours"], 8.5)
|
||||
|
||||
os.remove("test_export.json")
|
||||
|
||||
@patch('tamigo.select_date_range')
|
||||
@patch('tamigo.get_worked_days')
|
||||
@patch('questionary.select')
|
||||
@patch('questionary.text')
|
||||
@patch('tamigo.console')
|
||||
def test_export_worked_hours_xlsx(self, mock_console, mock_text, mock_select, mock_get_worked, mock_date_range):
|
||||
from tamigo import export_worked_hours
|
||||
|
||||
mock_date_range.return_value = (datetime(2025, 3, 1), datetime(2025, 3, 15))
|
||||
mock_get_worked.return_value = {
|
||||
"2025-03-11": {"hours": 8.5, "text": "Work"},
|
||||
"2025-03-10": {"hours": 8.0, "text": "Work"}
|
||||
}
|
||||
|
||||
mock_select.return_value.ask.return_value = "XLSX"
|
||||
mock_text.return_value.ask.return_value = "test_export.xlsx"
|
||||
|
||||
export_worked_hours(self.client)
|
||||
|
||||
self.assertTrue(os.path.exists("test_export.xlsx"))
|
||||
# We could use openpyxl to verify content if we really wanted to
|
||||
import openpyxl
|
||||
wb = openpyxl.load_workbook("test_export.xlsx")
|
||||
ws = wb.active
|
||||
self.assertEqual(ws.cell(row=1, column=1).value, "Date")
|
||||
self.assertEqual(ws.cell(row=2, column=1).value, "2025-03-10")
|
||||
self.assertEqual(ws.cell(row=2, column=2).value, 8.0)
|
||||
|
||||
os.remove("test_export.xlsx")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user