@@ -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