-
Notifications
You must be signed in to change notification settings - Fork 26k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FIX] project : prevent mismatch with analytic account and their plan #190746
base: 18.0
Are you sure you want to change the base?
[FIX] project : prevent mismatch with analytic account and their plan #190746
Conversation
bace06c
to
e8df0e1
Compare
e8287cf
to
a9a5569
Compare
9c6764e
to
7e928ca
Compare
7e928ca
to
ff2fc97
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your good work @damrOdoo
I left comments and questions for you 😄 Do not hesitate to reach me out if something is unclear!
Also, don't forget to add all the modules impacted in the commit title :)
@api.constrains('plan_id') | ||
def _ensure_project_plan_consistency(self): | ||
domain = expression.OR([ | ||
[(fnames, 'in', self.ids)] for fnames in self.env['project.project']._get_plan_fnames() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[(fnames, 'in', self.ids)] for fnames in self.env['project.project']._get_plan_fnames() | |
[(fname, 'in', self.ids)] for fname in self.env['project.project']._get_plan_fnames() |
@@ -475,6 +475,23 @@ def name_create(self, name): | |||
def create(self, vals_list): | |||
# Prevent double project creation | |||
self = self.with_context(mail_create_nosubscribe=True) | |||
# check that the analytic account are set on the correct plan. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of making the check in the create()
and write()
, we could do it once for all in this constraint: https://github.com/odoo/odoo/blob/18.0/addons/project/models/project_project.py#L963
We would iterate over each project and their accounts and check whether the accounts's plans are the same as the plans they should belong to.
Also, I suggest to use the root_plan_id
field (instead of plan_id
) to make comparisons, as we only allowed selecting AAs belonging to root plans in the different views.
We could also make the check generic and implement it in 'analytic.plan.fields.mixin'
directly but not sure tho.
account_record = self.env['account.analytic.account'].browse(account_id) | ||
account_per_account_id[account_id] = account_record | ||
if account_record.plan_id._column_name() != plan_column_id: | ||
raise UserError(f"The account '{account_record.name}' is linked to the plan '{account_record.plan_id.name}'. " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may want to translate the error message.
cls.analytic_plan = cls.env['account.analytic.plan'].create({ | ||
'name': 'Timesheet Plan Test', | ||
}) | ||
cls.project_plan = cls.env['account.analytic.plan'].browse(int(cls.env['ir.config_parameter'].get_param('analytic.project_plan', 0))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cls.project_plan = cls.env['account.analytic.plan'].browse(int(cls.env['ir.config_parameter'].get_param('analytic.project_plan', 0))) | |
cls.project_plan, _other_plans = cls.env['account.analytic.plan']._get_all_plans() |
I feel like it's more convenient to do it this way, even if we don't use _other_plans
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree its a bit more readable, but the browse is literally the first line executed by the get_all_plan function. We might as well do it ourselves and save us the following search of the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well okay but maybe we could keep my suggestion and use cls.other_plans
in test_mandatory_plan_timesheet_applicability
, so we avoid creating a new plan for nothing :)
]) | ||
project = self.env['project.project'].search(domain, limit=1) | ||
if project: | ||
raise ValidationError(_("At least one of the account is linked to a project. Unlink it from its project(s) before updating its plan.")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice for the user to see which projects (names) are impacted by the change of plans.
project.account_id = self.analytic_account_1 | ||
project_plan = self.env['account.analytic.plan'].browse(int(self.env['ir.config_parameter'].get_param('analytic.project_plan', 0))) | ||
|
||
project.account_id= self.env['account.analytic.account'].create({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
project.account_id= self.env['account.analytic.account'].create({ | |
project.account_id = self.env['account.analytic.account'].create({ |
@@ -16,9 +16,10 @@ def setUpClass(cls): | |||
cls.analytic_plan = cls.env['account.analytic.plan'].create({ | |||
'name': 'Plan Test', | |||
}) | |||
cls.project_plan = cls.env['account.analytic.plan'].browse(int(cls.env['ir.config_parameter'].get_param('analytic.project_plan', 0))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cls.project_plan = cls.env['account.analytic.plan'].browse(int(cls.env['ir.config_parameter'].get_param('analytic.project_plan', 0))) | |
cls.project_plan, _other_plans = cls.env['account.analytic.plan']._get_all_plans() |
'name': 'account_2', 'company_id': self.env.company.id, 'plan_id': analytic_plan.id | ||
}]) | ||
|
||
with self.assertRaises(UserError): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can add a message to the assertion to make it clearer why it should fail
@tagged('-at_install', 'post_install') | ||
class TestAnalyticAccountConsistency(TestProjectCommon): | ||
|
||
def test_account_consistency_orm_method(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def test_account_consistency_orm_method(self): | |
def test_account_plan_with_project_consistency(self): |
analytic_plan = self.env['account.analytic.plan'].create({'name': 'secondary plan'}) | ||
project_plan = self.env['account.analytic.plan'].browse(int(self.env['ir.config_parameter'].get_param('analytic.project_plan', 0))) | ||
account_project_plan, account_second_plan = self.env['account.analytic.account'].create([{ | ||
'name': 'account_1', 'company_id': self.env.company.id, 'plan_id': project_plan.id | ||
}, { | ||
'name': 'account_2', 'company_id': self.env.company.id, 'plan_id': analytic_plan.id | ||
}]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move these in the setUpClass()
6f40be4
to
9297270
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the changes @damrOdoo 💯
Second batch of comments :)
raise ValidationError(_(("At least one of the accounts is linked to the following projects : %s. " | ||
"Unlink it from its project(s) before updating its plan."), ', '.join(projects.mapped('name')))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
raise ValidationError(_(("At least one of the accounts is linked to the following projects : %s. " | |
"Unlink it from its project(s) before updating its plan."), ', '.join(projects.mapped('name')))) | |
raise ValidationError(_(( | |
"At least one of the accounts is linked to the following projects : %(projects_names)s. " | |
"Unlink it from its project(s) before updating its plan."), | |
projects_names=format_list(self.env, projects.mapped('name')), | |
)) |
I think format_list
is the new standard for that kind of stuff
domain = expression.OR([ | ||
[(fname, 'in', self.ids)] for fname in self.env['project.project']._get_plan_fnames() | ||
]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
domain = expression.OR([ | |
[(fname, 'in', self.ids)] for fname in self.env['project.project']._get_plan_fnames() | |
]) | |
domain = [('auto_account_id', 'in', self.ids)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One remark, if we change the plan of an account linked to a project, but the new plan belongs to the same root plan (it's a sub plan of that plan), I don't think we should trigger any Error as it's still consistent.
Let's take an example:
- We have one root plan 'Project' and two sub plans 'Sub Project 1' and 'Sub Project 2'
- We have one project with an account 'Account A' defined on the plan 'Project/Sub Project 1'
=> We want to change the plan of the account 'Account A' to 'Sub Project 2'
=> IMO, that should not trigger any error because they belong to the same root plan
(if we keep that, let's add a test)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't aware of the sub plans feature. I agree with your suggestion on this point. I'll made the change & tests.
About the auto_account_id field however, i dont think we can use it in this case. We're using it for a search on project, and since we only have roots plan, if the account is linked to a sub-plan, then the search will trigger an error, since we'll be searching on a non-existent field on projects. (if i correctly understood the how auto_account_id field works)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will work because we accept accounts from sub plans in the domain of analytic plan fields of project.project
.
Example: the field account_id
of project.project
can contain accounts from the 'Project' plan but also accounts from sub plans of 'Project' :)
@@ -475,6 +475,7 @@ def name_create(self, name): | |||
def create(self, vals_list): | |||
# Prevent double project creation | |||
self = self.with_context(mail_create_nosubscribe=True) | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Useless diff 🤷♂️
fnames = self._get_plan_fnames() | ||
domain = OR([[(fname, 'in', account_ids)] for fname in fnames]) | ||
projects = self.search(domain) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's search on the auto_account_id
field instead 😄
fnames = self._get_plan_fnames() | ||
domain = OR([[(fname, 'in', account_ids)] for fname in fnames]) | ||
projects = self.search(domain) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
""" | ||
:param account_ids: the account selected on the list view/ the active account of the form view | ||
:return: projects_name_per_account: a dict with 2 keys (account_id, other_plan) which contains the names of | ||
the project on which one of the account are set. This separation is done in order to display an accurate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the project on which one of the account are set. This separation is done in order to display an accurate | |
the project on which one of the accounts is set. This separation is done in order to display an accurate |
account = project[plan_column_id] | ||
if not account: | ||
continue | ||
if account.plan_id._column_name() != plan_column_id: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if account.plan_id._column_name() != plan_column_id: | |
if account.root_plan_id._column_name() != plan_column_id: |
We want to compare the root plans, as _get_plan_fnames()
only returns root plans.
(There exists sub plans but we only allowed to link AAs to root plans in the project views)
if not account: | ||
continue | ||
if account.plan_id._column_name() != plan_column_id: | ||
raise UserError(_('The account %(account_name)s is linked to the plan %(plan_name)s. You are not allowed to set it on other plan on your projects.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
raise UserError(_('The account %(account_name)s is linked to the plan %(plan_name)s. You are not allowed to set it on other plan on your projects.', | |
raise UserError(_('The account %(account_name)s is linked to the root plan %(plan_name)s. You are not allowed to set it on other root plan on your projects.', |
@@ -971,7 +1006,16 @@ def _get_projects_to_make_billable_domain(self): | |||
@api.constrains(lambda self: self._get_plan_fnames()) | |||
def _check_account_id(self): | |||
# Overriden from 'analytic.plan.fields.mixin' | |||
pass | |||
# check that the analytic account are set on the correct plan. | |||
plan_column_ids = self._get_plan_fnames() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
plan_column_ids = self._get_plan_fnames() | |
plan_column_names = self._get_plan_fnames() |
cls.analytic_plan = cls.env['account.analytic.plan'].create({ | ||
'name': 'Timesheet Plan Test', | ||
}) | ||
cls.project_plan = cls.env['account.analytic.plan'].browse(int(cls.env['ir.config_parameter'].get_param('analytic.project_plan', 0))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well okay but maybe we could keep my suggestion and use cls.other_plans
in test_mandatory_plan_timesheet_applicability
, so we avoid creating a new plan for nothing :)
99a9055
to
dfb244a
Compare
Currently, when the plan of an analytic account is updated, there is no sanity check, nor any computation done on the project accounts. This leads to account being linked to the wrong plan in the project setting, as well as strange computation result in the project profitability. step to reproduce : - open project setting - go to the analytic folder - If there are no analytic account on the project plan, add a new one - go to the form view of the analytic account - change the plan from project to department (or any other plan) - go back to the project setting -> analytic => the analytic account is still set on the 'project plan' field of the project. Solution : adds a warning message for the user when he tries to update the plan of analytic account from the form or the list view of analytic account. If the user confirms his update, the account are removed from their associated project. An sql constraint was also added in case some use case update the analytic plan without making use of the form view or the list view. task - 4334520 affected version : 18.0 - master
dfb244a
to
8c67351
Compare
Currently, when the plan of an analytic account is updated, there is no sanity check, nor any computation done on the project accounts. This leads to account being linked to the wrong plan in the project setting, as well as strange computation result in the project profitability.
step to reproduce :
=> the analytic account is still set on the 'project plan' field of the project.
Solution : adds a warning message for the user when he tries to update the plan of analytic account from the form or the list view of analytic account. If the user confirms his update, the account are removed from their associated project. An sql constraint was also added in case some use case update the analytic plan without making use of the form view or the list view.
task - 4334520
affected version : 18.0 - master