papi-projects 0.4.2__tar.gz → 0.4.4__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.2 → papi_projects-0.4.4}/PKG-INFO +1 -1
  2. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi/__init__.py +13 -0
  3. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi/wrappers.py +23 -12
  4. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi_projects.egg-info/PKG-INFO +1 -1
  5. {papi_projects-0.4.2 → papi_projects-0.4.4}/pyproject.toml +1 -1
  6. {papi_projects-0.4.2 → papi_projects-0.4.4}/scripts/collate_toggl_hours.py +11 -2
  7. papi_projects-0.4.4/scripts/generate_timesheet.py +242 -0
  8. papi_projects-0.4.2/scripts/generate_timesheet.py +0 -154
  9. {papi_projects-0.4.2 → papi_projects-0.4.4}/README.md +0 -0
  10. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi/project.py +0 -0
  11. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi/task.py +0 -0
  12. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi/user.py +0 -0
  13. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi/workorder.py +0 -0
  14. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi_projects.egg-info/SOURCES.txt +0 -0
  15. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi_projects.egg-info/dependency_links.txt +0 -0
  16. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi_projects.egg-info/entry_points.txt +0 -0
  17. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi_projects.egg-info/requires.txt +0 -0
  18. {papi_projects-0.4.2 → papi_projects-0.4.4}/papi_projects.egg-info/top_level.txt +0 -0
  19. {papi_projects-0.4.2 → papi_projects-0.4.4}/scripts/__init__.py +0 -0
  20. {papi_projects-0.4.2 → papi_projects-0.4.4}/scripts/create_notion_project.py +0 -0
  21. {papi_projects-0.4.2 → papi_projects-0.4.4}/scripts/create_notion_task.py +0 -0
  22. {papi_projects-0.4.2 → papi_projects-0.4.4}/scripts/create_project.py +0 -0
  23. {papi_projects-0.4.2 → papi_projects-0.4.4}/scripts/create_toggl_project.py +0 -0
  24. {papi_projects-0.4.2 → papi_projects-0.4.4}/scripts/test_notion.py +0 -0
  25. {papi_projects-0.4.2 → papi_projects-0.4.4}/setup.cfg +0 -0
  26. {papi_projects-0.4.2 → papi_projects-0.4.4}/tests/test_project.py +0 -0
  27. {papi_projects-0.4.2 → papi_projects-0.4.4}/tests/test_task.py +0 -0
  28. {papi_projects-0.4.2 → papi_projects-0.4.4}/tests/test_user.py +0 -0
  29. {papi_projects-0.4.2 → papi_projects-0.4.4}/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.2
3
+ Version: 0.4.4
4
4
  Summary: PAPI is an API for managing projects
5
5
  Author-email: sandyjmacdonald <sandyjmacdonald@gmail.com>
6
6
  License-Expression: MIT
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import logging
3
+ from decimal import Decimal, ROUND_UP
3
4
  from pathlib import Path
4
5
 
5
6
  try:
@@ -81,6 +82,18 @@ if missing:
81
82
  raise RuntimeError(f"Missing required config values: {', '.join(missing)}")
82
83
 
83
84
 
85
+ def round_up_to_multiple(hours: float, multiple: float = 7.5) -> float:
86
+ """Round hours up to the nearest multiple of `multiple` (default 7.5).
87
+
88
+ Values that are already an exact multiple are left unchanged, e.g. 7.5 -> 7.5,
89
+ 1 -> 7.5, 10 -> 15. Uses Decimal to avoid floating-point rounding surprises.
90
+ """
91
+ h = Decimal(str(hours))
92
+ m = Decimal(str(multiple))
93
+ multiples = (h / m).quantize(Decimal("1"), rounding=ROUND_UP)
94
+ return float(multiples * m)
95
+
96
+
84
97
  def setup_logger(enable_logging: bool, log_level: str = 'INFO', log_file: str = None):
85
98
  logger = logging.getLogger('papi')
86
99
 
@@ -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)
@@ -1069,20 +1077,23 @@ class NotionWrapper(Protocol):
1069
1077
 
1070
1078
  props = page.get("properties", {})
1071
1079
 
1080
+ nid = page.get("id")
1072
1081
  try:
1073
1082
  pid = project_id or props["Project ID"]["title"][0]["plain_text"]
1074
1083
  name = props["Description"]["rich_text"][0]["plain_text"]
1075
- owner = [u["name"] for u in props["Owner"]["people"]]
1084
+ owner = [u.get("name") for u in props["Owner"]["people"] if u.get("name")]
1076
1085
  status = props["Status"]["status"]["name"]
1077
1086
  priority = props["Priority"]["select"]["name"]
1078
1087
  user_id = props["PI Code"]["rollup"]["array"][0]["rich_text"][0]["plain_text"]
1079
- nid = page["id"]
1080
1088
  if len(props["Workorder"]["relation"]):
1081
1089
  workorder_page_id = props["Workorder"]["relation"][0]["id"]
1082
1090
  else:
1083
1091
  workorder_page_id = None
1084
1092
  except (KeyError, IndexError) as e:
1085
- logger.error(f"Malformed project properties on page {nid!r}: {e!r}")
1093
+ logger.error(
1094
+ f"Malformed project properties for Project ID={project_id!r} "
1095
+ f"on Notion page {nid!r}: {e!r}"
1096
+ )
1086
1097
  return None
1087
1098
 
1088
1099
  if workorders_db_id is not None and workorder_page_id is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: papi-projects
3
- Version: 0.4.2
3
+ Version: 0.4.4
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.2"
3
+ version = "0.4.4"
4
4
  description = "PAPI is an API for managing projects"
5
5
  authors = [{ name = "sandyjmacdonald", email = "sandyjmacdonald@gmail.com" }]
6
6
  license = "MIT"
@@ -2,7 +2,7 @@ import argparse
2
2
  import pendulum
3
3
  import warnings
4
4
  from papi.wrappers import TogglTrackWrapper
5
- from papi import config
5
+ from papi import config, round_up_to_multiple
6
6
 
7
7
  def main():
8
8
  """Main function of collate-toggl-hours script"""
@@ -22,6 +22,11 @@ def main():
22
22
  help="output TSV filename, omit to write to stdout",
23
23
  default=False,
24
24
  )
25
+ parser.add_argument(
26
+ "--round",
27
+ action="store_true",
28
+ help="round each project's total hours up to the nearest 7.5",
29
+ )
25
30
  args = parser.parse_args()
26
31
 
27
32
  # Set up start/end date
@@ -51,9 +56,13 @@ def main():
51
56
  # Get tracked hours and tracked project IDs/names
52
57
 
53
58
  tracked_hours = toggl.get_user_hours(start_time=start_time, end_time=end_time)
59
+ print(tracked_hours)
54
60
  projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
55
61
  hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
56
62
 
63
+ if args.round:
64
+ hours_per_project = [(name, round_up_to_multiple(hours)) for name, hours in hours_per_project]
65
+
57
66
  output = args.output
58
67
 
59
68
  if output:
@@ -69,4 +78,4 @@ def main():
69
78
  warnings.warn("Start time must not be more than 3 months ago!")
70
79
 
71
80
  if __name__ == "__main__":
72
- main()
81
+ main()
@@ -0,0 +1,242 @@
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, round_up_to_multiple
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
+ if not tracked_hours:
55
+ return {}
56
+ projects = {p["id"]: p["name"] for p in toggl.get_user_projects() if p["id"] in tracked_hours}
57
+ hours_per_project = [(projects[t], tracked_hours[t]) for t in tracked_hours]
58
+ hours_dict = {}
59
+ for i, p in enumerate(hours_per_project):
60
+ project_id = get_project_ids([p[0]])
61
+ if len(project_id):
62
+ hours_dict[project_id[0]] = hours_per_project[i][1]
63
+ else:
64
+ hours_dict[p[0]] = hours_per_project[i][1]
65
+ return hours_dict
66
+
67
+ def main():
68
+ parser = argparse.ArgumentParser()
69
+ parser.add_argument("-s", "--start", type=str, required=True)
70
+ parser.add_argument("-e", "--end", type=str, default=False)
71
+ parser.add_argument("--disable-logging", action="store_true")
72
+ parser.add_argument(
73
+ "--round",
74
+ action="store_true",
75
+ help="round each project's total hours (across all users) up to the nearest 7.5",
76
+ )
77
+ parser.add_argument("-o", "--output", type=str, required=True)
78
+ args = parser.parse_args()
79
+
80
+ if not args.disable_logging:
81
+ logger = setup_logger(True, "WARNING", None)
82
+
83
+ start_time = pendulum.parse(args.start).to_rfc3339_string()
84
+ # Toggl's end_date is exclusive, so add a day to include entries on the end date itself.
85
+ end_time = pendulum.now().to_rfc3339_string() if not args.end else pendulum.parse(args.end).add(days=1).to_rfc3339_string()
86
+
87
+ if pendulum.now() < pendulum.parse(args.start).add(months=3):
88
+ toggl_workspace = config["TOGGL_TRACK_WORKSPACE"]
89
+ toggl_users = load_toggl_users_from_toml()
90
+ if not toggl_users:
91
+ toggl_users = [{"name": "Self", "api_key": config["TOGGL_TRACK_API_KEY"]}]
92
+
93
+ notion = NotionWrapper(config["NOTION_API_SECRET"])
94
+ output = args.output
95
+
96
+ df = pd.DataFrame(
97
+ columns=[
98
+ "workorder",
99
+ "project_id",
100
+ "payment_type",
101
+ "costing_rate",
102
+ "hourly_rate",
103
+ "hours",
104
+ "cost",
105
+ "person",
106
+ "description",
107
+ "agresso_description",
108
+ ]
109
+ )
110
+
111
+ for user in toggl_users:
112
+ print(user)
113
+ toggl = initialise_toggl(user["api_key"], toggl_workspace)
114
+ try:
115
+ hours_dict = get_toggl_hours(start_time, end_time, toggl)
116
+ except Exception:
117
+ logger.error(f"[Toggl] Failed fetching hours for user {user['name']!r}")
118
+ raise
119
+ if not hours_dict:
120
+ logger.warning(
121
+ f"[Toggl] No hours tracked for user {user['name']!r} "
122
+ f"in the requested period; skipping."
123
+ )
124
+ continue
125
+ print(hours_dict)
126
+
127
+ for project_id, hours in hours_dict.items():
128
+ hours = float(hours)
129
+ payment_type = ""
130
+ costing_rate = ""
131
+ hourly_rate = ""
132
+ cost = 0.0
133
+ workorder_id = ""
134
+ if check_project_id(project_id):
135
+ try:
136
+ project = notion.get_project(
137
+ config["NOTION_PROJECTS_DB"],
138
+ project_id,
139
+ workorders_db_id=config["NOTION_WORKORDERS_DB"]
140
+ )
141
+ if project is not None:
142
+ project_name = project.name
143
+ workorder_id = project.workorder
144
+ if workorder_id is not None:
145
+ workorder = notion.get_workorder(
146
+ workorders_db_id=config["NOTION_WORKORDERS_DB"],
147
+ workorder_id=workorder_id
148
+ )
149
+ if workorder.is_complete():
150
+ payment_type = workorder.payment_type
151
+ costing_rate = workorder.costing_rate
152
+ hourly_rate = workorder.hourly_rate
153
+ cost = calculate_cost(hours, workorder.hourly_rate)
154
+ else:
155
+ project_name = ""
156
+ workorder_id = ""
157
+ except Exception:
158
+ logger.error(
159
+ f"[Notion] Failed processing project {project_id!r} "
160
+ f"for user {user['name']!r}"
161
+ )
162
+ raise
163
+ else:
164
+ project_name = project_id
165
+
166
+ df.loc[len(df)] = [
167
+ workorder_id,
168
+ project_id,
169
+ payment_type,
170
+ costing_rate,
171
+ hourly_rate,
172
+ hours,
173
+ cost,
174
+ user["name"],
175
+ project_name,
176
+ f"{project_id}: {project_name}",
177
+ ]
178
+
179
+ output_path = Path(output).expanduser()
180
+ itemised_path = output_path.with_name(f"{output_path.stem}-itemised{output_path.suffix}")
181
+ totals_path = output_path.with_name(f"{output_path.stem}-totals{output_path.suffix}")
182
+ da_path = output_path.with_name(f"{output_path.stem}-DA{output_path.suffix}")
183
+ di_path = output_path.with_name(f"{output_path.stem}-DI{output_path.suffix}")
184
+
185
+ df.to_csv(itemised_path, sep="\t", index=False)
186
+
187
+ totals_df = df.drop(columns=["person"]).groupby("project_id", as_index=False).agg(
188
+ {
189
+ "workorder": "first",
190
+ "payment_type": "first",
191
+ "costing_rate": "first",
192
+ "hourly_rate": "first",
193
+ "hours": "sum",
194
+ "cost": "sum",
195
+ "description": "first",
196
+ "agresso_description": "first",
197
+ }
198
+ )
199
+ totals_df = totals_df[
200
+ [
201
+ "workorder",
202
+ "project_id",
203
+ "payment_type",
204
+ "costing_rate",
205
+ "hourly_rate",
206
+ "hours",
207
+ "cost",
208
+ "description",
209
+ "agresso_description",
210
+ ]
211
+ ]
212
+
213
+ if args.round:
214
+ totals_df["hours"] = totals_df["hours"].apply(round_up_to_multiple)
215
+ totals_df["cost"] = totals_df.apply(
216
+ lambda row: calculate_cost(row["hours"], row["hourly_rate"])
217
+ if row["hourly_rate"] not in ("", None)
218
+ else row["cost"],
219
+ axis=1,
220
+ )
221
+
222
+ totals_df.to_csv(totals_path, sep="\t", index=False)
223
+
224
+ da_df = totals_df[totals_df["payment_type"] == "DA"]
225
+ if len(da_df):
226
+ da_df.to_csv(da_path, sep="\t", index=False)
227
+
228
+ di_df = totals_df[totals_df["payment_type"] == "DI"]
229
+ if len(di_df):
230
+ di_df.to_csv(di_path, sep="\t", index=False)
231
+
232
+ logger.warning(f"{len(df)} journal entries successfully written to {itemised_path}")
233
+ logger.warning(f"{len(totals_df)} project totals successfully written to {totals_path}")
234
+ if len(da_df):
235
+ logger.warning(f"{len(da_df)} DA totals successfully written to {da_path}")
236
+ if len(di_df):
237
+ logger.warning(f"{len(di_df)} DI totals successfully written to {di_path}")
238
+ else:
239
+ logger.warning("Start time must not be more than 3 months ago!")
240
+
241
+ if __name__ == "__main__":
242
+ main()
@@ -1,154 +0,0 @@
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
- end_time = pendulum.now().to_rfc3339_string() if not args.end else pendulum.parse(args.end).to_rfc3339_string()
78
-
79
- if pendulum.now() < pendulum.parse(args.start).add(months=3):
80
- toggl_workspace = config["TOGGL_TRACK_WORKSPACE"]
81
- toggl_users = load_toggl_users_from_toml()
82
- if not toggl_users:
83
- toggl_users = [{"name": "Self", "api_key": config["TOGGL_TRACK_API_KEY"]}]
84
-
85
- notion = NotionWrapper(config["NOTION_API_SECRET"])
86
- output = args.output
87
-
88
- df = pd.DataFrame(
89
- columns=[
90
- "workorder",
91
- "project_id",
92
- "payment_type",
93
- "costing_rate",
94
- "hourly_rate",
95
- "hours",
96
- "cost",
97
- "person",
98
- "description",
99
- "agresso_description",
100
- ]
101
- )
102
-
103
- for user in toggl_users:
104
- toggl = initialise_toggl(user["api_key"], toggl_workspace)
105
- hours_dict = get_toggl_hours(start_time, end_time, toggl)
106
-
107
- for project_id, hours in hours_dict.items():
108
- hours = float(hours)
109
- payment_type = ""
110
- costing_rate = ""
111
- hourly_rate = ""
112
- cost = 0.0
113
- workorder_id = ""
114
- if check_project_id(project_id):
115
- project = notion.get_project(
116
- config["NOTION_PROJECTS_DB"],
117
- project_id,
118
- workorders_db_id=config["NOTION_WORKORDERS_DB"]
119
- )
120
- project_name = project.name
121
- workorder_id = project.workorder
122
- if workorder_id is not None:
123
- workorder = notion.get_workorder(
124
- workorders_db_id=config["NOTION_WORKORDERS_DB"],
125
- workorder_id=workorder_id
126
- )
127
- if workorder.is_complete():
128
- payment_type = workorder.payment_type
129
- costing_rate = workorder.costing_rate
130
- hourly_rate = workorder.hourly_rate
131
- cost = calculate_cost(hours, workorder.hourly_rate)
132
- else:
133
- project_name = project_id
134
-
135
- df.loc[len(df)] = [
136
- workorder_id,
137
- project_id,
138
- payment_type,
139
- costing_rate,
140
- hourly_rate,
141
- hours,
142
- cost,
143
- user["name"],
144
- project_name,
145
- f"{project_id}: {project_name}",
146
- ]
147
-
148
- df.to_csv(output, sep="\t", index=False)
149
- logger.warning(f"{len(df)} journal entries successfully written to {output}")
150
- else:
151
- logger.warning("Start time must not be more than 3 months ago!")
152
-
153
- if __name__ == "__main__":
154
- main()
File without changes
File without changes