Skip to content

Commit cc3f4cc

Browse files
committed
fix(projects): refresh weekly project labels and clear stale list filters
1 parent 1f892b1 commit cc3f4cc

6 files changed

Lines changed: 137 additions & 4 deletions

File tree

time_tracking/time_tracking/doctype/time_tracking_profile/test_time_tracking_profile.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,23 @@ def test_employee_can_load_profile_ui_settings_without_settings_read_access(self
120120

121121
self.assertIn("track_target_adjustments", settings)
122122
self.assertIn("require_project_assignment", settings)
123+
124+
def test_duplicate_project_assignment_error_uses_project_label(self):
125+
user = self._make_user("duplicate-project")
126+
parent_label = f"Parent-{frappe.generate_hash(length=6)}"
127+
parent = self._make_project(parent_label)
128+
project = frappe.get_doc(
129+
{
130+
"doctype": "Time Tracking Project",
131+
"project_name": f"Child-{frappe.generate_hash(length=6)}",
132+
"parent_time_tracking_project": parent,
133+
}
134+
).insert(ignore_permissions=True)
135+
136+
frappe.set_user("Administrator")
137+
with self.assertRaises(frappe.ValidationError) as exc:
138+
self._make_profile(user, [project.name, project.name])
139+
140+
self.assertIn(parent_label, str(exc.exception))
141+
self.assertIn(project.project_name, str(exc.exception))
142+
self.assertNotIn(project.name, str(exc.exception))

time_tracking/time_tracking/doctype/time_tracking_profile/time_tracking_profile.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from frappe.utils import cint, flt, now_datetime
55

66
from time_tracking.time_tracking.doctype.time_tracking_project.time_tracking_project import (
7+
build_project_path_labels,
78
expand_project_assignments,
89
)
910
from time_tracking.time_tracking.vacation_utils import get_default_workdays_per_week
@@ -210,7 +211,10 @@ def _validate_project_assignment_duplicates(self):
210211
seen.add(project)
211212

212213
if duplicates:
213-
project_list = ", ".join(sorted(duplicates))
214+
project_labels = build_project_path_labels(list(duplicates))
215+
project_list = ", ".join(
216+
sorted(project_labels.get(project, project) for project in duplicates)
217+
)
214218
frappe.throw(_("Project {0} is already assigned to this profile.").format(project_list))
215219

216220
def _validate_project_assignment_permissions(self):

time_tracking/time_tracking/doctype/time_tracking_project/time_tracking_project_list.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,39 @@ frappe.listview_settings["Time Tracking Project"] = {
2525
});
2626
};
2727

28+
const removeInvalidParentFilters = () => {
29+
const area = filterArea();
30+
if (!area || !area.filters) {
31+
return false;
32+
}
33+
34+
let changed = false;
35+
const filters = area.filters.slice();
36+
filters.forEach((filter) => {
37+
const filterField = filter.fieldname || (filter.df && filter.df.fieldname) || "";
38+
const rawValue =
39+
filter.value !== undefined
40+
? filter.value
41+
: filter.get_value
42+
? filter.get_value()
43+
: filter[3];
44+
if (filterField !== "parent_time_tracking_project") {
45+
return;
46+
}
47+
if (rawValue !== "" && rawValue !== null && rawValue !== "Time Tracking Project") {
48+
return;
49+
}
50+
changed = true;
51+
if (area.remove_filter) {
52+
area.remove_filter(filter);
53+
} else if (filter.remove) {
54+
filter.remove();
55+
}
56+
});
57+
58+
return changed;
59+
};
60+
2861
const addFilter = (fieldname, value) => {
2962
const area = filterArea();
3063
if (!area) {
@@ -66,6 +99,10 @@ frappe.listview_settings["Time Tracking Project"] = {
6699
applyFilter("not_bookable", 1);
67100
});
68101
listview.page.add_inner_button(__("Clear Filters"), clearFilters);
102+
103+
if (removeInvalidParentFilters()) {
104+
listview.refresh();
105+
}
69106
},
70107
get_indicator(doc) {
71108
const status = (doc.project_status || "Active").trim();

time_tracking/time_tracking/page/weekly_booking/test_weekly_booking.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,31 @@ def test_group_project_is_available_and_bookable_when_setting_enabled(self):
241241

242242
result = get_weekly_booking(week_start_date="2026-04-13")
243243
self.assertTrue(any(row["project"] == group for row in result["rows"]))
244+
245+
def test_existing_unassigned_booking_includes_project_label(self):
246+
user = self._make_user("legacy-project")
247+
assigned_project = self._make_project(f"Assigned-{frappe.generate_hash(length=6)}")
248+
legacy_project = self._make_project(f"Legacy-{frappe.generate_hash(length=6)}")
249+
legacy_project_label = frappe.db.get_value(
250+
"Time Tracking Project", legacy_project, "project_name"
251+
)
252+
self._make_profile(user, assigned_project)
253+
254+
frappe.set_user(user)
255+
frappe.get_doc(
256+
{
257+
"doctype": "Time Booking",
258+
"time_tracking_profile": user,
259+
"date": "2026-04-13",
260+
"project": legacy_project,
261+
"duration_minutes": 60,
262+
"notes": "Imported legacy booking",
263+
}
264+
).insert(ignore_permissions=True)
265+
266+
assigned_projects = get_assigned_projects(user=user)
267+
self.assertNotIn(legacy_project, {project.name for project in assigned_projects})
268+
269+
result = get_weekly_booking(week_start_date="2026-04-13")
270+
self.assertTrue(any(row["project"] == legacy_project for row in result["rows"]))
271+
self.assertEqual(result["project_labels"][legacy_project], legacy_project_label)

time_tracking/time_tracking/page/weekly_booking/weekly_booking.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ frappe.pages["weekly-booking"].on_page_load = function (wrapper) {
337337
const state = {
338338
projects_loaded: false,
339339
projects: [],
340+
project_labels: {},
340341
increment_minutes: 15,
341342
day_label_format: "DD.MM.YYYY",
342343
weekly_target_hours: null,
@@ -606,12 +607,20 @@ frappe.pages["weekly-booking"].on_page_load = function (wrapper) {
606607

607608
if (selected && !selectedFound) {
608609
const value = escape(selected);
609-
options.push(`<option value="${value}" selected>${value}</option>`);
610+
const fallbackLabel = escape(state.project_labels[selected] || selected);
611+
options.push(`<option value="${value}" selected>${fallbackLabel}</option>`);
610612
}
611613

612614
return options.join("");
613615
}
614616

617+
function mergeProjectLabels(labels) {
618+
if (!labels || typeof labels !== "object") {
619+
return;
620+
}
621+
state.project_labels = Object.assign({}, state.project_labels, labels);
622+
}
623+
615624
function normalizeBillType(value) {
616625
return value === BILL_TYPE_UNBILLABLE ? BILL_TYPE_UNBILLABLE : BILL_TYPE_BILLABLE;
617626
}
@@ -1090,6 +1099,14 @@ frappe.pages["weekly-booking"].on_page_load = function (wrapper) {
10901099
}
10911100
const projects = Array.isArray(message) ? message : message.projects || [];
10921101
state.projects = projects;
1102+
mergeProjectLabels(
1103+
projects.reduce((acc, project) => {
1104+
if (project && project.name) {
1105+
acc[project.name] = project.path_label || project.project_name || project.name;
1106+
}
1107+
return acc;
1108+
}, {})
1109+
);
10931110
state.projects_loaded = true;
10941111
refreshProjectSelectOptions();
10951112
$addRowButton.prop("disabled", false);
@@ -1109,7 +1126,7 @@ frappe.pages["weekly-booking"].on_page_load = function (wrapper) {
11091126
}
11101127
state.projects_loaded = false;
11111128
if (document.body && document.body.dataset.route === "weekly-booking") {
1112-
loadProjects({ force_reload: true });
1129+
loadWeek();
11131130
}
11141131
}
11151132

@@ -1234,6 +1251,8 @@ frappe.pages["weekly-booking"].on_page_load = function (wrapper) {
12341251
updateDayHeaders();
12351252

12361253
const rows = message.rows || [];
1254+
state.project_labels = {};
1255+
mergeProjectLabels(message.project_labels || {});
12371256
const suggestions = buildSuggestionRows(message.previous_week_rows || [], rows);
12381257
state.loaded_week_total_minutes = calculateWeekMinutes(rows);
12391258
state.saved_row_counts = buildSavedRowCounts(rows);

time_tracking/time_tracking/page/weekly_booking/weekly_booking.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def _get_assigned_projects(user, profile_name=None):
178178
fields=["name", "project_name"],
179179
)
180180
project_map = {project.name: project for project in projects}
181-
path_labels = build_project_path_labels(list(project_map.keys()))
181+
path_labels = _get_project_labels(list(project_map.keys()))
182182
ordered = []
183183
for name in project_names:
184184
if name not in project_map:
@@ -189,6 +189,25 @@ def _get_assigned_projects(user, profile_name=None):
189189
return ordered
190190

191191

192+
def _get_project_labels(project_names):
193+
project_names = [name for name in project_names if name]
194+
if not project_names:
195+
return {}
196+
197+
projects = frappe.get_all(
198+
"Time Tracking Project",
199+
filters={"name": ["in", project_names]},
200+
fields=["name", "project_name"],
201+
)
202+
project_map = {project.name: project for project in projects}
203+
path_labels = build_project_path_labels(list(project_map.keys()))
204+
205+
return {
206+
name: path_labels.get(name, project.project_name or name)
207+
for name, project in project_map.items()
208+
}
209+
210+
192211
def _get_assigned_project_names(user):
193212
return {project.name for project in _get_assigned_projects(user)}
194213

@@ -347,6 +366,12 @@ def get_weekly_booking(user=None, week_start_date=None):
347366
)
348367

349368
response["vacation"] = _get_vacation_summary(profile_name, week_start_date)
369+
project_names = {
370+
row.get("project")
371+
for row in [*response["rows"], *response["previous_week_rows"]]
372+
if row.get("project")
373+
}
374+
response["project_labels"] = _get_project_labels(list(project_names))
350375
return response
351376

352377

0 commit comments

Comments
 (0)