papi-projects 0.4.1__tar.gz → 0.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {papi_projects-0.4.1 → papi_projects-0.4.3}/PKG-INFO +1 -1
  2. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi/__init__.py +44 -29
  3. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi/wrappers.py +18 -10
  4. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi_projects.egg-info/PKG-INFO +1 -1
  5. {papi_projects-0.4.1 → papi_projects-0.4.3}/pyproject.toml +1 -1
  6. {papi_projects-0.4.1 → papi_projects-0.4.3}/scripts/collate_toggl_hours.py +2 -1
  7. papi_projects-0.4.3/scripts/generate_timesheet.py +208 -0
  8. papi_projects-0.4.1/scripts/generate_timesheet.py +0 -194
  9. {papi_projects-0.4.1 → papi_projects-0.4.3}/README.md +0 -0
  10. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi/project.py +0 -0
  11. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi/task.py +0 -0
  12. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi/user.py +0 -0
  13. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi/workorder.py +0 -0
  14. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi_projects.egg-info/SOURCES.txt +0 -0
  15. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi_projects.egg-info/dependency_links.txt +0 -0
  16. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi_projects.egg-info/entry_points.txt +0 -0
  17. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi_projects.egg-info/requires.txt +0 -0
  18. {papi_projects-0.4.1 → papi_projects-0.4.3}/papi_projects.egg-info/top_level.txt +0 -0
  19. {papi_projects-0.4.1 → papi_projects-0.4.3}/scripts/__init__.py +0 -0
  20. {papi_projects-0.4.1 → papi_projects-0.4.3}/scripts/create_notion_project.py +0 -0
  21. {papi_projects-0.4.1 → papi_projects-0.4.3}/scripts/create_notion_task.py +0 -0
  22. {papi_projects-0.4.1 → papi_projects-0.4.3}/scripts/create_project.py +0 -0
  23. {papi_projects-0.4.1 → papi_projects-0.4.3}/scripts/create_toggl_project.py +0 -0
  24. {papi_projects-0.4.1 → papi_projects-0.4.3}/scripts/test_notion.py +0 -0
  25. {papi_projects-0.4.1 → papi_projects-0.4.3}/setup.cfg +0 -0
  26. {papi_projects-0.4.1 → papi_projects-0.4.3}/tests/test_project.py +0 -0
  27. {papi_projects-0.4.1 → papi_projects-0.4.3}/tests/test_task.py +0 -0
  28. {papi_projects-0.4.1 → papi_projects-0.4.3}/tests/test_user.py +0 -0
  29. {papi_projects-0.4.1 → papi_projects-0.4.3}/tests/test_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papi-projects
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: PAPI is an API for managing projects
5
5
  Author-email: sandyjmacdonald <sandyjmacdonald@gmail.com>
6
6
  License-Expression: MIT
@@ -1,41 +1,60 @@
1
1
  import os
2
2
  import logging
3
- from dotenv import dotenv_values
4
3
  from pathlib import Path
5
4
 
5
+ try:
6
+ import tomllib
7
+ except ModuleNotFoundError:
8
+ import tomli as tomllib
9
+
10
+
6
11
  def load_config():
7
- user_env_path = Path.home() / ".config" / "papi" / ".env"
12
+ config_path = os.environ.get("PAPI_CONFIG_FILE")
13
+ if config_path:
14
+ path = Path(config_path).expanduser()
15
+ else:
16
+ path = Path.home() / ".config" / "papi" / "config.toml"
17
+
18
+ if not path.exists():
19
+ raise RuntimeError(f"Config file not found: {path}")
20
+
21
+ with path.open("rb") as f:
22
+ raw = tomllib.load(f)
23
+
8
24
  config = {}
9
25
 
10
- if user_env_path.exists():
11
- config.update(dotenv_values(user_env_path))
12
-
13
- for key in (
14
- "TOGGL_TRACK_API_KEY",
15
- "TOGGL_TRACK_PASSWORD",
16
- "NOTION_API_SECRET",
17
- "NOTION_CLIENTS_DB",
18
- "NOTION_PROJECTS_DB",
19
- "NOTION_TASKS_DB",
20
- "NOTION_TEMPLATE_PAGE_ID",
21
- "NOTION_WORKORDERS_DB",
22
- "NOTION_TRAC_COSTS_DB",
23
- ):
26
+ toggl = raw.get("toggl", {})
27
+ notion = raw.get("notion", {})
28
+
29
+ mapping = {
30
+ "TOGGL_TRACK_API_KEY": toggl.get("api_key"),
31
+ "TOGGL_TRACK_PASSWORD": toggl.get("password"),
32
+ "TOGGL_TRACK_WORKSPACE": toggl.get("workspace"),
33
+ "NOTION_API_SECRET": notion.get("api_secret"),
34
+ "NOTION_CLIENTS_DB": notion.get("clients_db"),
35
+ "NOTION_PROJECTS_DB": notion.get("projects_db"),
36
+ "NOTION_TASKS_DB": notion.get("tasks_db"),
37
+ "NOTION_TEMPLATE_PAGE_ID": notion.get("template_page_id"),
38
+ "NOTION_WORKORDERS_DB": notion.get("workorders_db"),
39
+ "NOTION_TRAC_COSTS_DB": notion.get("trac_costs_db"),
40
+ }
41
+
42
+ for k, v in mapping.items():
43
+ if v is not None:
44
+ config[k] = v
45
+
46
+ for key in mapping.keys():
24
47
  if key in os.environ:
25
48
  config[key] = os.environ[key]
26
49
 
27
- local_env_path = Path(__file__).parent / ".env"
28
- if local_env_path.exists():
29
- local_vals = dotenv_values(local_env_path)
30
- for k, v in local_vals.items():
31
- config.setdefault(k, v)
32
-
33
50
  return config
34
51
 
52
+
35
53
  config = load_config()
36
54
 
37
55
  TOGGL_TRACK_API_KEY = config["TOGGL_TRACK_API_KEY"]
38
56
  TOGGL_TRACK_PASSWORD = config["TOGGL_TRACK_PASSWORD"]
57
+ TOGGL_TRACK_WORKSPACE = config["TOGGL_TRACK_WORKSPACE"]
39
58
  NOTION_API_SECRET = config["NOTION_API_SECRET"]
40
59
  NOTION_CLIENTS_DB = config["NOTION_CLIENTS_DB"]
41
60
  NOTION_PROJECTS_DB = config["NOTION_PROJECTS_DB"]
@@ -47,6 +66,7 @@ NOTION_TRAC_COSTS_DB = config["NOTION_TRAC_COSTS_DB"]
47
66
  required = [
48
67
  "TOGGL_TRACK_API_KEY",
49
68
  "TOGGL_TRACK_PASSWORD",
69
+ "TOGGL_TRACK_WORKSPACE",
50
70
  "NOTION_API_SECRET",
51
71
  "NOTION_CLIENTS_DB",
52
72
  "NOTION_PROJECTS_DB",
@@ -60,15 +80,14 @@ missing = [k for k in required if k not in config]
60
80
  if missing:
61
81
  raise RuntimeError(f"Missing required config values: {', '.join(missing)}")
62
82
 
83
+
63
84
  def setup_logger(enable_logging: bool, log_level: str = 'INFO', log_file: str = None):
64
85
  logger = logging.getLogger('papi')
65
86
 
66
87
  if enable_logging:
67
- # Convert log_level string to logging level
68
88
  numeric_level = getattr(logging, log_level.upper(), logging.INFO)
69
89
  logger.setLevel(numeric_level)
70
-
71
- # Create handler
90
+
72
91
  if log_file:
73
92
  handler = logging.FileHandler(log_file)
74
93
  else:
@@ -76,18 +95,14 @@ def setup_logger(enable_logging: bool, log_level: str = 'INFO', log_file: str =
76
95
 
77
96
  handler.setLevel(numeric_level)
78
97
 
79
- # Create formatter
80
98
  formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
81
99
  handler.setFormatter(formatter)
82
100
 
83
- # Add handler to logger if not already added
84
101
  if not logger.handlers:
85
102
  logger.addHandler(handler)
86
103
 
87
- # Prevent propagation to root logger
88
104
  logger.propagate = False
89
105
  else:
90
- # Set a higher log level to suppress lower-level logs
91
106
  logger.setLevel(logging.WARNING)
92
107
 
93
108
  return logger
@@ -176,14 +176,18 @@ class TogglTrackWrapper(Protocol):
176
176
  if isinstance(times_json, list):
177
177
  times = {}
178
178
  for t in times_json:
179
- pid = t["pid"]
180
- seconds = t["duration"]
181
- hours = seconds / 60 / 60
182
- if pid not in times:
183
- times[pid] = hours
184
- else:
185
- times[pid] += hours
186
- return times
179
+ if "pid" in t:
180
+ pid = t["pid"]
181
+ seconds = t["duration"]
182
+ hours = seconds / 60 / 60
183
+ if pid not in times:
184
+ times[pid] = hours
185
+ else:
186
+ times[pid] += hours
187
+ if times:
188
+ return times
189
+ else:
190
+ return None
187
191
  else:
188
192
  warnings.warn(times_json)
189
193
  return None
@@ -419,7 +423,11 @@ class NotionWrapper(Protocol):
419
423
  t = b["type"]
420
424
  if t not in ALLOWED:
421
425
  continue
422
- minimal = { "type": t, t: b[t] }
426
+ block_data = {
427
+ k: v for k, v in b[t].items()
428
+ if v is not None and not (k == "color" and v == "default")
429
+ }
430
+ minimal = {"type": t, t: block_data}
423
431
  if b.get("has_children"):
424
432
  child_raw = self._fetch_template_blocks(b["id"])
425
433
  minimal["children"] = self._clean_blocks(child_raw)
@@ -910,7 +918,7 @@ class NotionWrapper(Protocol):
910
918
  "people": []
911
919
  },
912
920
  "Status": {
913
- "status": {"name": "Not Started"}
921
+ "status": {"name": "In Development"}
914
922
  },
915
923
  "Priority": {
916
924
  "select": {"name": "Medium"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papi-projects
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: PAPI is an API for managing projects
5
5
  Author-email: sandyjmacdonald <sandyjmacdonald@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "papi-projects"
3
- version = "0.4.1"
3
+ version = "0.4.3"
4
4
  description = "PAPI is an API for managing projects"
5
5
  authors = [{ name = "sandyjmacdonald", email = "sandyjmacdonald@gmail.com" }]
6
6
  license = "MIT"
@@ -51,6 +51,7 @@ def main():
51
51
  # Get tracked hours and tracked project IDs/names
52
52
 
53
53
  tracked_hours = toggl.get_user_hours(start_time=start_time, end_time=end_time)
54
+ print(tracked_hours)
54
55
  projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
55
56
  hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
56
57
 
@@ -69,4 +70,4 @@ def main():
69
70
  warnings.warn("Start time must not be more than 3 months ago!")
70
71
 
71
72
  if __name__ == "__main__":
72
- main()
73
+ main()
@@ -0,0 +1,208 @@
1
+ import argparse
2
+ import pendulum
3
+ import warnings
4
+ import os
5
+ from pathlib import Path
6
+ from papi.wrappers import TogglTrackWrapper, NotionWrapper
7
+ from papi import config, setup_logger
8
+ from papi.project import get_project_ids, check_project_id
9
+ from decimal import Decimal, ROUND_UP
10
+ import pandas as pd
11
+
12
+ try:
13
+ import tomllib
14
+ except ModuleNotFoundError:
15
+ import tomli as tomllib
16
+
17
+ def load_toggl_users_from_toml():
18
+ config_path = os.environ.get("PAPI_CONFIG_FILE")
19
+ if config_path:
20
+ path = Path(config_path).expanduser()
21
+ else:
22
+ path = Path.home() / ".config" / "papi" / "config.toml"
23
+ if not path.exists():
24
+ return []
25
+ with path.open("rb") as f:
26
+ raw = tomllib.load(f)
27
+ toggl = raw.get("toggl", {})
28
+ users = toggl.get("users", {})
29
+ out = []
30
+ if isinstance(users, dict):
31
+ for _, u in users.items():
32
+ if not isinstance(u, dict):
33
+ continue
34
+ name = u.get("name")
35
+ api_key = u.get("api_key")
36
+ if name and api_key:
37
+ out.append({"name": name, "api_key": api_key})
38
+ return out
39
+
40
+ def initialise_toggl(toggl_api_key, toggl_workspace):
41
+ toggl_api_password = config["TOGGL_TRACK_PASSWORD"]
42
+ toggl = TogglTrackWrapper(toggl_api_key, toggl_api_password)
43
+ toggl.set_default_workspace(toggl_workspace)
44
+ toggl.set_me()
45
+ return toggl
46
+
47
+ def calculate_cost(hours: float, hourly_rate: float) -> float:
48
+ cost = Decimal(hours * hourly_rate)
49
+ cost_rounded = cost.quantize(Decimal("0.01"), rounding=ROUND_UP)
50
+ return cost_rounded
51
+
52
+ def get_toggl_hours(start_time, end_time, toggl):
53
+ tracked_hours = toggl.get_user_hours(start_time=start_time, end_time=end_time)
54
+ projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
55
+ hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
56
+ hours_dict = {}
57
+ for i, p in enumerate(hours_per_project):
58
+ project_id = get_project_ids([p[0]])
59
+ if len(project_id):
60
+ hours_dict[project_id[0]] = hours_per_project[i][1]
61
+ else:
62
+ hours_dict[p[0]] = hours_per_project[i][1]
63
+ return hours_dict
64
+
65
+ def main():
66
+ parser = argparse.ArgumentParser()
67
+ parser.add_argument("-s", "--start", type=str, required=True)
68
+ parser.add_argument("-e", "--end", type=str, default=False)
69
+ parser.add_argument("--disable-logging", action="store_true")
70
+ parser.add_argument("-o", "--output", type=str, required=True)
71
+ args = parser.parse_args()
72
+
73
+ if not args.disable_logging:
74
+ logger = setup_logger(True, "WARNING", None)
75
+
76
+ start_time = pendulum.parse(args.start).to_rfc3339_string()
77
+ # Toggl's end_date is exclusive, so add a day to include entries on the end date itself.
78
+ end_time = pendulum.now().to_rfc3339_string() if not args.end else pendulum.parse(args.end).add(days=1).to_rfc3339_string()
79
+
80
+ if pendulum.now() < pendulum.parse(args.start).add(months=3):
81
+ toggl_workspace = config["TOGGL_TRACK_WORKSPACE"]
82
+ toggl_users = load_toggl_users_from_toml()
83
+ if not toggl_users:
84
+ toggl_users = [{"name": "Self", "api_key": config["TOGGL_TRACK_API_KEY"]}]
85
+
86
+ notion = NotionWrapper(config["NOTION_API_SECRET"])
87
+ output = args.output
88
+
89
+ df = pd.DataFrame(
90
+ columns=[
91
+ "workorder",
92
+ "project_id",
93
+ "payment_type",
94
+ "costing_rate",
95
+ "hourly_rate",
96
+ "hours",
97
+ "cost",
98
+ "person",
99
+ "description",
100
+ "agresso_description",
101
+ ]
102
+ )
103
+
104
+ for user in toggl_users:
105
+ print(user)
106
+ toggl = initialise_toggl(user["api_key"], toggl_workspace)
107
+ hours_dict = get_toggl_hours(start_time, end_time, toggl)
108
+ print(hours_dict)
109
+
110
+ for project_id, hours in hours_dict.items():
111
+ hours = float(hours)
112
+ payment_type = ""
113
+ costing_rate = ""
114
+ hourly_rate = ""
115
+ cost = 0.0
116
+ workorder_id = ""
117
+ if check_project_id(project_id):
118
+ project = notion.get_project(
119
+ config["NOTION_PROJECTS_DB"],
120
+ project_id,
121
+ workorders_db_id=config["NOTION_WORKORDERS_DB"]
122
+ )
123
+ if project is not None:
124
+ project_name = project.name
125
+ workorder_id = project.workorder
126
+ if workorder_id is not None:
127
+ workorder = notion.get_workorder(
128
+ workorders_db_id=config["NOTION_WORKORDERS_DB"],
129
+ workorder_id=workorder_id
130
+ )
131
+ if workorder.is_complete():
132
+ payment_type = workorder.payment_type
133
+ costing_rate = workorder.costing_rate
134
+ hourly_rate = workorder.hourly_rate
135
+ cost = calculate_cost(hours, workorder.hourly_rate)
136
+ else:
137
+ project_name = ""
138
+ workorder_id = ""
139
+ else:
140
+ project_name = project_id
141
+
142
+ df.loc[len(df)] = [
143
+ workorder_id,
144
+ project_id,
145
+ payment_type,
146
+ costing_rate,
147
+ hourly_rate,
148
+ hours,
149
+ cost,
150
+ user["name"],
151
+ project_name,
152
+ f"{project_id}: {project_name}",
153
+ ]
154
+
155
+ output_path = Path(output).expanduser()
156
+ itemised_path = output_path.with_name(f"{output_path.stem}-itemised{output_path.suffix}")
157
+ totals_path = output_path.with_name(f"{output_path.stem}-totals{output_path.suffix}")
158
+ da_path = output_path.with_name(f"{output_path.stem}-DA{output_path.suffix}")
159
+ di_path = output_path.with_name(f"{output_path.stem}-DI{output_path.suffix}")
160
+
161
+ df.to_csv(itemised_path, sep="\t", index=False)
162
+
163
+ totals_df = df.drop(columns=["person"]).groupby("project_id", as_index=False).agg(
164
+ {
165
+ "workorder": "first",
166
+ "payment_type": "first",
167
+ "costing_rate": "first",
168
+ "hourly_rate": "first",
169
+ "hours": "sum",
170
+ "cost": "sum",
171
+ "description": "first",
172
+ "agresso_description": "first",
173
+ }
174
+ )
175
+ totals_df = totals_df[
176
+ [
177
+ "workorder",
178
+ "project_id",
179
+ "payment_type",
180
+ "costing_rate",
181
+ "hourly_rate",
182
+ "hours",
183
+ "cost",
184
+ "description",
185
+ "agresso_description",
186
+ ]
187
+ ]
188
+ totals_df.to_csv(totals_path, sep="\t", index=False)
189
+
190
+ da_df = totals_df[totals_df["payment_type"] == "DA"]
191
+ if len(da_df):
192
+ da_df.to_csv(da_path, sep="\t", index=False)
193
+
194
+ di_df = totals_df[totals_df["payment_type"] == "DI"]
195
+ if len(di_df):
196
+ di_df.to_csv(di_path, sep="\t", index=False)
197
+
198
+ logger.warning(f"{len(df)} journal entries successfully written to {itemised_path}")
199
+ logger.warning(f"{len(totals_df)} project totals successfully written to {totals_path}")
200
+ if len(da_df):
201
+ logger.warning(f"{len(da_df)} DA totals successfully written to {da_path}")
202
+ if len(di_df):
203
+ logger.warning(f"{len(di_df)} DI totals successfully written to {di_path}")
204
+ else:
205
+ logger.warning("Start time must not be more than 3 months ago!")
206
+
207
+ if __name__ == "__main__":
208
+ main()
@@ -1,194 +0,0 @@
1
- import argparse
2
- import pendulum
3
- import warnings
4
- from papi.wrappers import TogglTrackWrapper, NotionWrapper
5
- from papi import config, setup_logger
6
- from papi.project import get_project_ids, check_project_id
7
- from decimal import Decimal, ROUND_UP
8
- import pandas as pd
9
-
10
- def initialise_toggl(toggl_api_key, toggl_workspace):
11
- """Initialises the TogglTrackWrapper.
12
-
13
- :param toggl_api_key: A user's Toggl Track API key.
14
- :type toggl_api_key: str
15
- :param toggl_workspace: The name of the Toggl Track workspace.
16
- :type toggl_workspace: str
17
- :return: An initialised TogglTrackWrapper.
18
- :rtype: TogglTrackWrapper
19
- """
20
- toggl_api_password = config["TOGGL_TRACK_PASSWORD"]
21
- toggl = TogglTrackWrapper(toggl_api_key, toggl_api_password)
22
- toggl.set_default_workspace(toggl_workspace)
23
- toggl.set_me()
24
- return toggl
25
-
26
- def calculate_cost(hours: float, hourly_rate: float) -> float:
27
- """Calculates a properly-rounded cost given hours and an hourly rate.
28
-
29
- :param hours: A number of hours, e.g. 1.5 for one and a half hours.
30
- :type hours: float
31
- :param hourly_rate: An hourly rate to charge.
32
- :type hourly_rate: float
33
- :return: A properly-rounded (note: rounded up) cost to charge.
34
- :rtype: float
35
- """
36
- cost = Decimal(hours * hourly_rate)
37
- cost_rounded = cost.quantize(Decimal("0.01"), rounding=ROUND_UP)
38
- return cost_rounded
39
-
40
- def get_toggl_hours(start_time, end_time, toggl):
41
- """Get the tracked hours between a start time and end time from an
42
- initialised TogglTrackWrapper instance.
43
-
44
- :param start_time: A start time, e.g. 2025-11-01
45
- :type start_time: str
46
- :param end_time: An end time, e.g. 2025-11-31
47
- :type end_time: str
48
- :param toggl: An initialised TogglTrackWrapper instance.
49
- :type toggl: TogglTrackWrapper
50
- :return: A dicitonary of projects and tracked hours.
51
- :rtype: dict
52
- """
53
- tracked_hours = toggl.get_user_hours(start_time=start_time, end_time=end_time)
54
- projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
55
- hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
56
- hours_dict = {}
57
- for i, p in enumerate(hours_per_project):
58
- project_id = get_project_ids([p[0]])
59
- if len(project_id):
60
- project_id = project_id[0]
61
- hours_dict[project_id] = hours_per_project[i][1]
62
- else:
63
- hours_dict[p[0]] = hours_per_project[i][1]
64
- return hours_dict
65
-
66
- def main():
67
- """Main function of generate-timesheet script"""
68
- # Set up argparse
69
- parser = argparse.ArgumentParser()
70
- parser.add_argument(
71
- "-s", "--start", type=str, help="start date in YYYY-MM-DD format", required=True
72
- )
73
- parser.add_argument(
74
- "-e", "--end", type=str, help="end date in YYYY-MM-DD format, if none supplied then end date is now", default=False
75
- )
76
- parser.add_argument(
77
- "--disable-logging",
78
- action="store_true",
79
- help="disable logging output for the papi library",
80
- )
81
- parser.add_argument(
82
- "-o",
83
- "--output",
84
- type=str,
85
- help="output TSV filename",
86
- default=False,
87
- required=True
88
- )
89
- args = parser.parse_args()
90
-
91
- if not args.disable_logging:
92
- logger = setup_logger(True, "WARNING", None)
93
-
94
- # Set up start/end date
95
- start_date = args.start
96
- start_time = pendulum.parse(start_date).to_rfc3339_string()
97
- end_date = args.end
98
- if not end_date:
99
- end_time = pendulum.now().to_rfc3339_string()
100
- else:
101
- end_time = pendulum.parse(end_date).to_rfc3339_string()
102
-
103
- # Check that start time is not more than 3 months ago
104
- if pendulum.now() < pendulum.parse(start_date).add(months=3):
105
- toggl_api_key = config["TOGGL_TRACK_API_KEY"]
106
- toggl_workspace = config["TOGGL_TRACK_WORKSPACE"]
107
- toggl = initialise_toggl(toggl_api_key, toggl_workspace)
108
-
109
- hours_dict = get_toggl_hours(start_time, end_time, toggl)
110
-
111
- notion_api_secret = config["NOTION_API_SECRET"]
112
- notion_clients_db = config["NOTION_CLIENTS_DB"]
113
- notion_projects_db = config["NOTION_PROJECTS_DB"]
114
- notion_workorders_db = config["NOTION_WORKORDERS_DB"]
115
- notion = NotionWrapper(notion_api_secret)
116
- project_ids = hours_dict.keys()
117
- output = args.output
118
- df = pd.DataFrame(
119
- columns=[
120
- "workorder",
121
- "project_id",
122
- "payment_type",
123
- "costing_rate",
124
- "hourly_rate",
125
- "hours",
126
- "cost",
127
- "description",
128
- "agresso_description",
129
- ]
130
- )
131
-
132
- for project_id in project_ids:
133
- hours = float(hours_dict[project_id])
134
- payment_type = ""
135
- costing_rate = ""
136
- hourly_rate = ""
137
- cost = 0.0
138
- chargeable = False
139
- workorder_id = ""
140
- if check_project_id(project_id):
141
- project = notion.get_project(
142
- notion_projects_db,
143
- project_id,
144
- workorders_db_id=notion_workorders_db
145
- )
146
- project_name = project.name
147
- workorder_id = project.workorder
148
- if workorder_id is not None:
149
- workorder = notion.get_workorder(
150
- workorders_db_id=notion_workorders_db,
151
- workorder_id=workorder_id
152
- )
153
- if workorder.is_complete():
154
- payment_type = workorder.payment_type
155
- costing_rate = workorder.costing_rate
156
- hourly_rate = workorder.hourly_rate
157
- cost = calculate_cost(hours, workorder.hourly_rate)
158
- chargeable = True
159
- logger.warning(
160
- f"Project: {project_id}: Has complete data and is chargeable"
161
- )
162
- else:
163
- logger.warning(
164
- f"Project {project_id}: Workorder {workorder.id} has incomplete data!"
165
- )
166
- else:
167
- workorder_id = ""
168
- logger.warning(f"Project {project_id}: Workorder is missing!")
169
- else:
170
- logger.warning(f"Project {project_id}: Not a valid project ID!")
171
- project_name = project_id
172
-
173
- agresso_description = f"{project_id}: {project_name}"
174
- description = project_name
175
-
176
- df.loc[len(df)] = [
177
- workorder_id,
178
- project_id,
179
- payment_type,
180
- costing_rate,
181
- hourly_rate,
182
- hours,
183
- cost,
184
- description,
185
- agresso_description,
186
- ]
187
-
188
- df.to_csv(output, sep="\t", index=False)
189
- logger.warning(f"{len(df)} journal entries successfully written to {output}")
190
- else:
191
- logger.warning("Start time must not be more than 3 months ago!")
192
-
193
- if __name__ == "__main__":
194
- main()
File without changes
File without changes