Skip to content

Commit 3698e81

Browse files
committed
[Add] Code coverage improvement
1 parent dd9d3ed commit 3698e81

File tree

2 files changed

+185
-66
lines changed

2 files changed

+185
-66
lines changed

project_forecast_line/tests/test_forecast_line.py

Lines changed: 46 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -344,70 +344,10 @@ def test_forecast_with_calendar(self):
344344
)
345345
self.assertFalse(to_remove_lines.exists())
346346

347-
@freeze_time("2022-01-01 12:00:00")
348-
def test_calendar_leave_write_updates_forecast_lines(self):
349-
calendar = self.employee_dev.resource_calendar_id
350-
351-
def _april_hours():
352-
line = self.ForecastLine.search(
353-
[
354-
("employee_id", "=", self.employee_dev.id),
355-
("forecast_role_id", "=", self.role_developer.id),
356-
("res_model", "=", self.role_model),
357-
("date_from", "=", "2022-04-01"),
358-
]
359-
)
360-
return line.forecast_hours if line else None
361-
362-
# Baseline: April = 21 working days × 8 h
363-
april_baseline = 21.0 * 8
364-
self.assertEqual(_april_hours(), april_baseline)
365-
366-
# Create a 1-day leave on April 18 → April shrinks by 8 h
367-
leave = self.env["resource.calendar.leaves"].create(
368-
{
369-
"name": "Easter Monday",
370-
"calendar_id": calendar.id,
371-
"date_from": "2022-04-18 00:00:00",
372-
"date_to": "2022-04-19 00:00:00",
373-
"time_type": "leave",
374-
}
375-
)
376-
self.env.flush_all()
377-
self.assertEqual(
378-
_april_hours(),
379-
(21.0 - 1) * 8,
380-
"Creating the leave must reduce April by 8 h",
381-
)
382-
383-
# Write: extend the leave to 2 days (April 18–20) → April shrinks by another 8 h
384-
leave.write({"date_to": "2022-04-20 00:00:00"})
385-
self.env.flush_all()
386-
387-
self.assertEqual(
388-
_april_hours(),
389-
(21.0 - 2) * 8,
390-
"After extending the leave via write(), April must lose another 8 h",
391-
)
392-
393347
@freeze_time("2022-01-01 12:00:00")
394348
def test_calendar_leave_unlink_updates_forecast_lines(self):
395-
"""Unlinking a resource.calendar.leaves record must trigger
396-
_update_forecast_lines() via the captured roles so that the
397-
leave's reduction on forecast hours is reversed.
398-
399-
Flow exercised:
400-
1. Capture the baseline April forecast hours for the developer (no leave).
401-
2. Create a one-day calendar leave on Easter Monday 18 Apr 2022 →
402-
the April forecast line must shrink by 8 h (1 working day).
403-
3. Unlink the leave → ResourceCalendarLeaves.unlink() saves the roles
404-
before deletion and calls roles._update_forecast_lines() after.
405-
4. Assert the April line is restored to its original value, proving
406-
that _update_forecast_lines was called with the correct roles.
407-
"""
408349
calendar = self.employee_dev.resource_calendar_id
409350

410-
# ── Baseline: April has 21 working days, no leave ──
411351
lines_before = self.ForecastLine.search(
412352
[
413353
("employee_id", "=", self.employee_dev.id),
@@ -419,8 +359,6 @@ def test_calendar_leave_unlink_updates_forecast_lines(self):
419359
self.assertEqual(len(lines_before), 1)
420360
expected_april_hours = 21.0 * 8 # 21 working days × 8 h
421361
self.assertEqual(lines_before.forecast_hours, expected_april_hours)
422-
423-
# ── Create a one-day leave on Easter Monday → April shrinks by 8 h ──
424362
leave = self.env["resource.calendar.leaves"].create(
425363
{
426364
"name": "Easter Monday",
@@ -446,12 +384,8 @@ def test_calendar_leave_unlink_updates_forecast_lines(self):
446384
(21.0 - 1) * 8,
447385
"Creating the leave must reduce the April forecast by 8 h",
448386
)
449-
450-
# ── Unlink the leave ────
451387
leave.unlink()
452388
self.env.flush_all()
453-
454-
# ── Assert the April line is fully restored ───
455389
april_after_unlink = self.ForecastLine.search(
456390
[
457391
("employee_id", "=", self.employee_dev.id),
@@ -468,6 +402,52 @@ def test_calendar_leave_unlink_updates_forecast_lines(self):
468402
"the April forecast to its original value",
469403
)
470404

405+
@freeze_time("2022-01-01 12:00:00")
406+
def test_calendar_leave_write_updates_forecast_lines(self):
407+
calendar = self.employee_dev.resource_calendar_id
408+
409+
def _april_hours():
410+
line = self.ForecastLine.search(
411+
[
412+
("employee_id", "=", self.employee_dev.id),
413+
("forecast_role_id", "=", self.role_developer.id),
414+
("res_model", "=", self.role_model),
415+
("date_from", "=", "2022-04-01"),
416+
]
417+
)
418+
return line.forecast_hours if line else None
419+
420+
# Baseline: April = 21 working days × 8 h
421+
april_baseline = 21.0 * 8
422+
self.assertEqual(_april_hours(), april_baseline)
423+
424+
# Create a 1-day leave on April 18 → April shrinks by 8 h
425+
leave = self.env["resource.calendar.leaves"].create(
426+
{
427+
"name": "Easter Monday",
428+
"calendar_id": calendar.id,
429+
"date_from": "2022-04-18 00:00:00",
430+
"date_to": "2022-04-19 00:00:00",
431+
"time_type": "leave",
432+
}
433+
)
434+
self.env.flush_all()
435+
self.assertEqual(
436+
_april_hours(),
437+
(21.0 - 1) * 8,
438+
"Creating the leave must reduce April by 8 h",
439+
)
440+
441+
# Write: extend the leave to 2 days (April 18–20) → April shrinks by another 8 h
442+
leave.write({"date_to": "2022-04-20 00:00:00"})
443+
self.env.flush_all()
444+
445+
self.assertEqual(
446+
_april_hours(),
447+
(21.0 - 2) * 8,
448+
"After extending the leave via write(), April must lose another 8 h",
449+
)
450+
471451
@freeze_time("2022-01-01 12:00:00")
472452
def test_calendar_leave_unlink_global_leave_updates_all_employee_roles(self):
473453
"""When a global leave (no resource_id) is unlinked, _get_resource_roles()

project_forecast_line/tests/test_project_task.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,29 @@ def test_task_onchange_user(self):
3131
self.env.flush_all()
3232
self.assertEqual(task.forecast_role_id.id, self.role_consultant.id)
3333

34+
def test_task_onchange_user_clear_users_clears_role(self):
35+
"""Removing all users from a task must clear forecast_role_id."""
36+
project = self.env["project.project"].create({"name": "Test Project ClearUser"})
37+
task = self.env["project.task"].new(
38+
{
39+
"name": "Test Task ClearUser",
40+
"project_id": project.id,
41+
"forecast_role_id": False,
42+
}
43+
)
44+
# Assign a user → role gets set via onchange
45+
task.user_ids = [(4, self.user_consultant.id)]
46+
task.onchange_user_ids()
47+
self.assertEqual(task.forecast_role_id.id, self.role_consultant.id)
48+
49+
# Remove all users → forecast_role_id must be cleared
50+
task.user_ids = [(5,)]
51+
task.onchange_user_ids()
52+
self.assertFalse(
53+
task.forecast_role_id,
54+
"forecast_role_id must be cleared when user_ids is empty",
55+
)
56+
3457
def test_task_quick_update_forecast(self):
3558
"""Test _quick_update_forecast_lines method logic"""
3659
project = self.ProjectProject.create({"name": "TestProjectQuick"})
@@ -69,6 +92,92 @@ def test_task_quick_update_forecast(self):
6992
self.env.invalidate_all()
7093
self.assertEqual(forecast_lines[0].forecast_hours, -2.0)
7194

95+
@freeze_time("2022-02-14 12:00:00")
96+
def test_quick_update_fallback_when_no_forecast_lines(self):
97+
"""When no forecast lines exist for a task, _quick_update_forecast_lines
98+
must fall back to _update_forecast_lines() and create them.
99+
"""
100+
project = self.ProjectProject.create({"name": "TestQuickFallback"})
101+
project.stage_id = self.env.ref("project.project_project_stage_1")
102+
task = self.ProjectTask.create(
103+
{
104+
"name": "No Lines Task",
105+
"project_id": project.id,
106+
"forecast_role_id": self.role_consultant.id,
107+
"forecast_date_planned_start": "2022-02-14",
108+
"forecast_date_planned_end": "2022-02-14",
109+
"allocated_hours": 8,
110+
"remaining_hours": 8,
111+
}
112+
)
113+
task.user_ids = self.user_consultant
114+
115+
# Confirm: no forecast lines exist yet
116+
lines_before = self.env["forecast.line"].search(
117+
[("res_model", "=", "project.task"), ("res_id", "=", task.id)]
118+
)
119+
self.assertFalse(lines_before)
120+
121+
# Call _quick_update_forecast_lines → no lines exist → fallback
122+
task._quick_update_forecast_lines()
123+
self.env.flush_all()
124+
self.env.invalidate_all()
125+
126+
lines_after = self.env["forecast.line"].search(
127+
[("res_model", "=", "project.task"), ("res_id", "=", task.id)]
128+
)
129+
self.assertTrue(
130+
lines_after,
131+
"_quick_update_forecast_lines must fall back to "
132+
"_update_forecast_lines when no forecast lines exist",
133+
)
134+
135+
@freeze_time("2022-02-14 12:00:00")
136+
def test_quick_update_fallback_when_total_forecast_zero(self):
137+
"""When forecast lines exist but total_forecast is zero,
138+
_quick_update_forecast_lines must fall back to
139+
_update_forecast_lines() (ratio division would be undefined).
140+
"""
141+
project = self.ProjectProject.create({"name": "TestQuickZero"})
142+
project.stage_id = self.env.ref("project.project_project_stage_1")
143+
task = self.ProjectTask.create(
144+
{
145+
"name": "Zero Total Task",
146+
"project_id": project.id,
147+
"forecast_role_id": self.role_consultant.id,
148+
"forecast_date_planned_start": "2022-02-14",
149+
"forecast_date_planned_end": "2022-02-14",
150+
"allocated_hours": 8,
151+
"remaining_hours": 8,
152+
}
153+
)
154+
task.user_ids = self.user_consultant
155+
task._update_forecast_lines()
156+
self.env.flush_all()
157+
158+
lines = self.env["forecast.line"].search(
159+
[("res_model", "=", "project.task"), ("res_id", "=", task.id)]
160+
)
161+
self.assertTrue(lines)
162+
163+
# Force total_forecast to zero by zeroing out all lines
164+
for line in lines:
165+
line.forecast_hours = 0.0
166+
self.env.flush_all()
167+
168+
# Call _quick_update → total_forecast is 0 → fallback
169+
task._quick_update_forecast_lines()
170+
self.env.flush_all()
171+
self.env.invalidate_all()
172+
173+
lines_after = self.env["forecast.line"].search(
174+
[("res_model", "=", "project.task"), ("res_id", "=", task.id)]
175+
)
176+
self.assertTrue(
177+
lines_after,
178+
"_quick_update_forecast_lines must fall back when total is zero",
179+
)
180+
72181
# ------------------------------------------------------------------
73182
# Tests for project.task._write (models/project_task.py L56-L62)
74183
# ------------------------------------------------------------------
@@ -609,3 +718,33 @@ def test_set_forecast_type(self):
609718
forecast_type = task_2.set_forecast_type()
610719
self.env.flush_all()
611720
self.assertEqual(forecast_type, "confirmed")
721+
722+
# Condition 3: Task with SO line in draft state → bare return (None)
723+
task_3, so_3 = self._make_task_with_sale_line(
724+
"SaleLineDraftReturn", so_state="draft", project_stage_id=False
725+
)
726+
task_3.project_id.stage_id = False
727+
forecast_type = task_3.set_forecast_type()
728+
self.assertIsNone(
729+
forecast_type,
730+
"set_forecast_type must return None when sale_line_id " "is in draft state",
731+
)
732+
733+
# Condition 4: No stage and no sale line → "forecast" (else branch)
734+
project_no_stage = self.ProjectProject.create({"name": "NoStageNoSale"})
735+
project_no_stage.stage_id = False
736+
task_4 = self.ProjectTask.create(
737+
{
738+
"name": "NoStageNoSaleTask",
739+
"project_id": project_no_stage.id,
740+
"forecast_role_id": self.role_consultant.id,
741+
}
742+
)
743+
self.assertFalse(task_4.sale_line_id)
744+
forecast_type = task_4.set_forecast_type()
745+
self.assertEqual(
746+
forecast_type,
747+
"forecast",
748+
"set_forecast_type must return 'forecast' when neither "
749+
"stage_id nor sale_line_id is set",
750+
)

0 commit comments

Comments
 (0)