Skip to content

Commit

Permalink
Merge pull request OCA#971 from yankinmax/project_forecast_line-diff-…
Browse files Browse the repository at this point in the history
…roles

[15.0][FIX] project_forecast_line: take roles into account on calc and add setting to control consumption states
  • Loading branch information
gurneyalex authored Sep 5, 2022
2 parents 471a0ae + 82f5f36 commit be5c59a
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 14 deletions.
30 changes: 26 additions & 4 deletions project_forecast_line/models/forecast_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,23 @@ class ForecastLine(models.Model):
"forecast.line", "employee_resource_forecast_line_id"
)

def _get_consumption_states(self):
consumption_states = self.env.company.forecast_consumption_states
return tuple(consumption_states.split("_"))

@api.depends("employee_id", "date_from", "type", "res_model")
def _compute_employee_forecast_line_id(self):
consumption_states = self._get_consumption_states()
employees = self.mapped("employee_id")
main_roles = employees.mapped("main_role_id")
date_froms = self.mapped("date_from")
date_tos = self.mapped("date_to")
forecast_roles = self.mapped("forecast_role_id") | main_roles
if employees:
lines = self.search(
[
("employee_id", "in", employees.ids),
("forecast_role_id", "in", forecast_roles.ids),
("res_model", "=", "hr.employee.forecast.role"),
("date_from", ">=", min(date_froms)),
("date_to", "<=", max(date_tos)),
Expand All @@ -104,12 +112,26 @@ def _compute_employee_forecast_line_id(self):
lines = self.env["forecast.line"]
capacities = {}
for line in lines:
capacities[(line.employee_id.id, line.date_from)] = line.id
capacities[
(line.employee_id.id, line.date_from, line.forecast_role_id.id)
] = line.id
for rec in self:
if rec.type == "confirmed" and rec.res_model != "hr.employee.forecast.role":
rec.employee_resource_forecast_line_id = capacities.get(
(rec.employee_id.id, rec.date_from), False
if (
rec.type in consumption_states
and rec.res_model != "hr.employee.forecast.role"
):
resource_forecast_line = capacities.get(
(rec.employee_id.id, rec.date_from, rec.forecast_role_id.id), False
)
if resource_forecast_line:
rec.employee_resource_forecast_line_id = resource_forecast_line
else:
# if we didn't find a forecast line with a matching role
# we get forecast line with the main role of the employee
main_role_id = rec.employee_id.main_role_id
rec.employee_resource_forecast_line_id = capacities.get(
(rec.employee_id.id, rec.date_from, main_role_id.id), False
)
else:
rec.employee_resource_forecast_line_id = False

Expand Down
5 changes: 3 additions & 2 deletions project_forecast_line/models/project_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class ProjectTask(models.Model):
def create(self, vals_list):
# compatibility with fields from project_enterprise
for vals in vals_list:
if "planned_date_begin" in vals:
if vals.get("planned_date_begin"):
vals["forecast_date_planned_start"] = vals["planned_date_begin"]
if "planned_date_end" in vals:
if vals.get("planned_date_end"):
vals["forecast_date_planned_end"] = vals["planned_date_end"]
tasks = super().create(vals_list)
tasks._update_forecast_lines()
Expand Down Expand Up @@ -94,6 +94,7 @@ def _update_forecast_lines(self):
# are not generating forecast lines from SO
_logger.info("skip task %s: draft sale")
continue

if (
not task.forecast_date_planned_start
or not task.forecast_date_planned_end
Expand Down
16 changes: 16 additions & 0 deletions project_forecast_line/models/res_company.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ class ResCompany(models.Model):
forecast_line_horizon = fields.Integer(
help="Number of month for the forecast planning", default=12
)
forecast_consumption_states = fields.Selection(
selection=[
("confirmed", "Compute consolidated forecast for lines of type confirmed"),
(
"forecast_confirmed",
"Include lines of type forecast in consolidated forecast computation",
),
],
string="Consumption state rules",
help="For instance, holidays requests and sales quotation lines"
"create lines of type forecast and won't be taken into account"
"during consolidated forecast computation, whereas tasks for project"
"which are in a running state create lines with type confirmed"
"and will be used to compute consolidated forecast.",
default="confirmed",
)

def write(self, values):
res = super().write(values)
Expand Down
4 changes: 3 additions & 1 deletion project_forecast_line/models/res_config_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ class ResConfigSettings(models.TransientModel):
forecast_line_horizon = fields.Integer(
related="company_id.forecast_line_horizon", readonly=False
)

forecast_consumption_states = fields.Selection(
related="company_id.forecast_consumption_states", readonly=False
)
group_forecast_line_on_quotation = fields.Boolean(
"Forecast Line on Quotations",
implied_group="project_forecast_line.group_forecast_line_on_quotation",
Expand Down
152 changes: 145 additions & 7 deletions project_forecast_line/tests/test_forecast_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ def test_task_forecast_lines_consolidated_forecast(self):
self.assertEqual(len(forecast), 1)
# using assertEqual on purpose here
self.assertEqual(forecast.forecast_hours, -6.0)
self.assertEqual(round(forecast.consolidated_forecast, 5), 0.75000)
self.assertAlmostEqual(forecast.consolidated_forecast, 0.75)
self.assertEqual(
forecast.employee_resource_forecast_line_id.consolidated_forecast,
0.25,
Expand Down Expand Up @@ -603,10 +603,148 @@ def test_task_forecast_lines_consolidated_forecast_overallocation_multiple_tasks
forecast1.employee_resource_forecast_line_id,
forecast2.employee_resource_forecast_line_id,
)
self.assertEqual(
round(
forecast1.employee_resource_forecast_line_id.consolidated_forecast,
5,
),
-0.75000,
self.assertAlmostEqual(
forecast1.employee_resource_forecast_line_id.consolidated_forecast,
-0.75,
)

def test_task_forecast_lines_employee_different_roles(self):
"""
Test forecast lines when employee has different roles.
Employee has 2 forecast_role_id: consultant 75% and project manager 25%,
working 8h per day (standard calendar).
Create a task with forecast role consultant, with remaining time = 8h
and a scheduled period starting and ending on the same day (today for instance).
Assign this task to the user.
Expected: for the user, on today, 3 forecast lines.
res_model forecast_role_id forecast_hours consolidated_forecast
project.task consultant -8 1 (in days)
hr.employee.forecast.role consultant 6 -0.25 (in days)
hr.employee.forecast.role project manager 2 0.25 (in days)
"""
self.env["hr.employee.forecast.role"].create(
{
"employee_id": self.employee_consultant.id,
"role_id": self.role_pm.id,
"date_start": "2022-01-01",
"rate": 25,
"sequence": 1,
}
)
consultant_role = self.env["hr.employee.forecast.role"].search(
[
("employee_id", "=", self.employee_consultant.id),
("role_id", "=", self.role_consultant.id),
]
)
consultant_role.rate = 75
project = self.env["project.project"].create({"name": "TestProjectDiffRoles"})
# set project in stage "in progress" to get confirmed forecast
project.stage_id = self.env.ref("project.project_project_stage_1")
task = self.env["project.task"].create(
{
"name": "TaskDiffRoles",
"project_id": project.id,
"forecast_role_id": self.role_consultant.id,
"forecast_date_planned_start": date.today(),
"forecast_date_planned_end": date.today(),
"planned_hours": 8,
}
)
task.user_ids = self.user_consultant
task_forecast = self.env["forecast.line"].search([("task_id", "=", task.id)])
self.assertEqual(len(task_forecast), 1)
# using assertEqual on purpose here
self.assertEqual(task_forecast.forecast_hours, -8.0)
self.assertEqual(task_forecast.consolidated_forecast, 1.0)
employee_forecast = self.env["forecast.line"].search(
[("employee_id", "=", self.employee_consultant.id)]
)
# we can take first line to check as forecast values are equal
forecast_consultant = employee_forecast.filtered(
lambda l: l.res_model == "hr.employee.forecast.role"
and l.forecast_role_id == self.role_consultant
)[0]
self.assertEqual(forecast_consultant.forecast_hours, 6.0)
self.assertAlmostEqual(forecast_consultant.consolidated_forecast, -0.25)
forecast_pm = employee_forecast.filtered(
lambda l: l.res_model == "hr.employee.forecast.role"
and l.forecast_role_id == self.role_pm
)[0]
self.assertEqual(forecast_pm.forecast_hours, 2.0)
self.assertAlmostEqual(forecast_pm.consolidated_forecast, 0.25)

def test_task_forecast_lines_employee_main_role(self):
"""
Test forecast lines when employee has different roles
and different from employee's role is assigned to the task.
Employee has 2 forecast_role_id: consultant 75% and project manager 25%,
working 8h per day (standard calendar).
Create a task with forecast role developer, with remaining time = 8h
and a scheduled period starting and ending on the same day (today for instance).
Assign this task to the user.
Expected: for the user, on today, 3 forecast lines.
res_model forecast_role_id forecast_hours consolidated_forecast
project.task consultant -8 1 (in days)
hr.employee.forecast.role consultant 6 -0.25 (in days)
hr.employee.forecast.role project manager 2 0.25 (in days)
"""
self.env["hr.employee.forecast.role"].create(
{
"employee_id": self.employee_consultant.id,
"role_id": self.role_pm.id,
"date_start": "2022-01-01",
"rate": 25,
"sequence": 1,
}
)
consultant_role = self.env["hr.employee.forecast.role"].search(
[
("employee_id", "=", self.employee_consultant.id),
("role_id", "=", self.role_consultant.id),
]
)
consultant_role.rate = 75
project = self.env["project.project"].create({"name": "TestProjectDiffRoles"})
# set project in stage "in progress" to get confirmed forecast
project.stage_id = self.env.ref("project.project_project_stage_1")
task = self.env["project.task"].create(
{
"name": "TaskDiffRoles",
"project_id": project.id,
"forecast_role_id": self.role_developer.id,
"forecast_date_planned_start": date.today(),
"forecast_date_planned_end": date.today(),
"planned_hours": 8,
}
)
task.user_ids = self.user_consultant
task_forecast = self.env["forecast.line"].search([("task_id", "=", task.id)])
self.assertEqual(len(task_forecast), 1)
# using assertEqual on purpose here
self.assertEqual(task_forecast.forecast_hours, -8.0)
self.assertEqual(task_forecast.consolidated_forecast, 1.0)
employee_forecast = self.env["forecast.line"].search(
[("employee_id", "=", self.employee_consultant.id)]
)
# we can take first line to check as forecast values are equal
forecast_consultant = employee_forecast.filtered(
lambda l: l.res_model == "hr.employee.forecast.role"
and l.forecast_role_id == self.role_consultant
)[0]
self.assertEqual(forecast_consultant.forecast_hours, 6.0)
self.assertAlmostEqual(forecast_consultant.consolidated_forecast, -0.25)
forecast_pm = employee_forecast.filtered(
lambda l: l.res_model == "hr.employee.forecast.role"
and l.forecast_role_id == self.role_pm
)[0]
self.assertEqual(forecast_pm.forecast_hours, 2.0)
self.assertAlmostEqual(forecast_pm.consolidated_forecast, 0.25)
11 changes: 11 additions & 0 deletions project_forecast_line/views/res_config_settings_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@
class="o_field_integer o_field_number o_field_widget o_input oe_inline col-lg-2"
/>
</div>
<div class="mt8">
<label for="forecast_consumption_states" />
<div class="text-muted">
Select the states for which the consumption is confirmed or not
</div>
<field
name="forecast_consumption_states"
required="1"
class="o_light_label"
/>
</div>
</div>
</div>
</div>
Expand Down

0 comments on commit be5c59a

Please sign in to comment.