From dd706ea024f757f359f0e557844e8a5329a235ea Mon Sep 17 00:00:00 2001 From: Thibault Libioulle Date: Mon, 5 Jul 2021 18:43:12 +0200 Subject: [PATCH] [IMP] (sale_)project,hr_timesheet: add analytic account and tags on task Previously, all costs allocated to a task were associated, if set, to the analytic account of their project. This commit adds a deeper level of granularity, as the user will be able to override the project analytic account and add analytic tags. When creating timesheets or creating account move line related to SO linked to a task, the analytic account of the task is now taken into account rather than the one defined on the project. If an analytic account is set on the SO, it remains the default account used. If a task is related to an SOL and an invoice line is created from this SOL, the analytic tags of the task are also given to the account move line. If different analytic account are linked to a same SOL through the project using this SOL or the tasks using this SOL, than no default analytic account is set on the invoice line. PR : #70527 task-2458135 --- addons/hr_timesheet/models/hr_timesheet.py | 14 +++++++- addons/hr_timesheet/models/project.py | 4 +-- addons/project/models/__init__.py | 1 + addons/project/models/analytic_account_tag.py | 9 ++++++ addons/project/models/project.py | 17 ++++++++++ .../project/models/project_task_recurrence.py | 2 +- addons/project/views/project_views.xml | 2 ++ addons/sale_project/models/sale_order.py | 32 ++++++++++++++----- addons/sale_timesheet/models/project.py | 8 ++--- .../views/project_task_views.xml | 3 -- 10 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 addons/project/models/analytic_account_tag.py diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index cbe9140b59a..9533ed792f3 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -4,7 +4,7 @@ from lxml import etree import re -from odoo import api, fields, models, _ +from odoo import api, Command, fields, models, _ from odoo.exceptions import UserError, AccessError from odoo.osv import expression @@ -178,6 +178,18 @@ class AccountAnalyticLine(models.Model): Overrride this to compute on the fly some field that can not be computed fields. :param values: dict values for `create`or `write`. """ + # task implies analytic account and tags + if vals.get('task_id') and not vals.get('account_id'): + task = self.env['project.task'].browse(vals.get('task_id')) + task_analytic_account_id = task._get_task_analytic_account_id() + vals['account_id'] = task_analytic_account_id.id + vals['company_id'] = task_analytic_account_id.company_id.id or task.company_id.id + if vals.get('tag_ids'): + vals['tag_ids'] += [Command.link(tag_id.id) for tag_id in task.analytic_tag_ids] + else: + vals['tag_ids'] = [Command.set(task.analytic_tag_ids.ids)] + if not task_analytic_account_id.active: + raise UserError(_('You cannot add timesheets to a project or a task linked to an inactive analytic account.')) # project implies analytic account if vals.get('project_id') and not vals.get('account_id'): project = self.env['project.project'].browse(vals.get('project_id')) diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index d7e4d41b4c9..96d41250b3f 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -204,11 +204,11 @@ class Task(models.Model): def _compute_encode_uom_in_days(self): self.encode_uom_in_days = self._uom_in_days() - @api.depends('project_id.analytic_account_id.active') + @api.depends('analytic_account_id.active', 'project_id.analytic_account_id.active') def _compute_analytic_account_active(self): """ Overridden in sale_timesheet """ for task in self: - task.analytic_account_active = task.project_id.analytic_account_id.active + task.analytic_account_active = task._get_task_analytic_account_id().active @api.depends('timesheet_ids.unit_amount') def _compute_effective_hours(self): diff --git a/addons/project/models/__init__.py b/addons/project/models/__init__.py index 83a5980fd1c..4bf11751b09 100644 --- a/addons/project/models/__init__.py +++ b/addons/project/models/__init__.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import analytic_account +from . import analytic_account_tag from . import project_milestone from . import project_project_stage from . import project_task_recurrence diff --git a/addons/project/models/analytic_account_tag.py b/addons/project/models/analytic_account_tag.py new file mode 100644 index 00000000000..f686c30dd9d --- /dev/null +++ b/addons/project/models/analytic_account_tag.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + +class AccountAnalyticTag(models.Model): + _inherit = 'account.analytic.tag' + + task_ids = fields.Many2many('project.task', string='Tasks') diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 009cdf542eb..b8895e7a484 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -1040,6 +1040,16 @@ class Task(models.Model): repeat_show_week = fields.Boolean(compute='_compute_repeat_visibility') repeat_show_month = fields.Boolean(compute='_compute_repeat_visibility') + # Account analytic + analytic_account_id = fields.Many2one('account.analytic.account', ondelete='set null', + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", check_company=True, + help="Analytic account to which this task is linked for financial management. " + "Use an analytic account to record cost and revenue on your task. " + "If empty, the analytic account of the project will be used.") + project_analytic_account_id = fields.Many2one('account.analytic.account', string='Project Analytic Account', related='project_id.analytic_account_id') + analytic_tag_ids = fields.Many2many('account.analytic.tag', + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", check_company=True) + @property def SELF_READABLE_FIELDS(self): return PROJECT_TASK_READABLE_FIELDS | self.SELF_WRITABLE_FIELDS @@ -1979,6 +1989,13 @@ class Task(models.Model): portal_users = task.message_partner_ids.user_ids.filtered('share') task.message_unsubscribe(partner_ids=portal_users.partner_id.ids) + # --------------------------------------------------- + # Analytic accounting + # --------------------------------------------------- + def _get_task_analytic_account_id(self): + self.ensure_one() + return self.analytic_account_id or self.project_analytic_account_id + class ProjectTags(models.Model): """ Tags of project's tasks """ _name = "project.tags" diff --git a/addons/project/models/project_task_recurrence.py b/addons/project/models/project_task_recurrence.py index 7a690690b14..f9964deb313 100644 --- a/addons/project/models/project_task_recurrence.py +++ b/addons/project/models/project_task_recurrence.py @@ -146,7 +146,7 @@ class ProjectTaskRecurrence(models.Model): return ['message_partner_ids', 'company_id', 'description', 'displayed_image_id', 'email_cc', 'parent_id', 'partner_email', 'partner_id', 'partner_phone', 'planned_hours', 'project_id', 'project_privacy_visibility', 'sequence', 'tag_ids', 'recurrence_id', - 'name', 'recurring_task'] + 'name', 'recurring_task', 'analytic_account_id'] def _get_weekdays(self, n=1): self.ensure_one() diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index b48ab46a6cb..ad4f69b3f52 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -930,6 +930,8 @@ + + diff --git a/addons/sale_project/models/sale_order.py b/addons/sale_project/models/sale_order.py index 10795b84a15..48e6062241e 100644 --- a/addons/sale_project/models/sale_order.py +++ b/addons/sale_project/models/sale_order.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models, _ +from odoo import api, Command, fields, models, _ from odoo.tools.safe_eval import safe_eval from odoo.tools.sql import column_exists, create_column @@ -362,15 +362,31 @@ class SaleOrderLine(models.Model): """ values = super(SaleOrderLine, self)._prepare_invoice_line(**optional_values) if not values['analytic_account_id']: - if self.project_id.analytic_account_id: - values['analytic_account_id'] = self.project_id.analytic_account_id - elif self.task_id.project_id.analytic_account_id: - values['analytic_account_id'] = self.task_id.project_id.analytic_account_id + if self.task_id.analytic_account_id: + values['analytic_account_id'] = self.task_id._get_task_analytic_account_id().id + elif self.project_id.analytic_account_id: + values['analytic_account_id'] = self.project_id.analytic_account_id.id elif self.is_service and not self.is_expense: - project_analytic_account_id = self.env['project.project'].read_group([ - '|', ('sale_line_id', '=', self.id), ('task_ids.sale_line_id', '=', self.id) + task_analytic_account_id = self.env['project.task'].read_group([ + ('sale_line_id', '=', self.id), + ('analytic_account_id', '!=', False), ], ['analytic_account_id'], ['analytic_account_id']) - analytic_account_ids = {rec['analytic_account_id'][0] for rec in project_analytic_account_id} + project_analytic_account_id = self.env['project.project'].read_group([ + ('analytic_account_id', '!=', False), + '|', + ('sale_line_id', '=', self.id), + '&', + ('tasks.sale_line_id', '=', self.id), + ('tasks.analytic_account_id', '=', False) + ], ['analytic_account_id'], ['analytic_account_id']) + analytic_account_ids = {rec['analytic_account_id'][0] for rec in (task_analytic_account_id + project_analytic_account_id)} if len(analytic_account_ids) == 1: values['analytic_account_id'] = analytic_account_ids.pop() + if self.task_id.analytic_tag_ids: + values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in self.task_id.analytic_tag_ids] + elif self.is_service and not self.is_expense: + tag_ids = self.env['account.analytic.tag'].search([ + ('task_ids.sale_line_id', '=', self.id) + ]) + values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in tag_ids] return values diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index 9af4cd514b6..217c9fe5040 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -347,7 +347,7 @@ class ProjectTask(models.Model): return res sale_order_id = fields.Many2one(domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]") - analytic_account_id = fields.Many2one('account.analytic.account', related='sale_order_id.analytic_account_id') + so_analytic_account_id = fields.Many2one(related='sale_order_id.analytic_account_id', string='Sale Order Analytic Account') pricing_type = fields.Selection(related="project_id.pricing_type") is_project_map_empty = fields.Boolean("Is Project map empty", compute='_compute_is_project_map_empty') has_multi_sol = fields.Boolean(compute='_compute_has_multi_sol', compute_sudo=True) @@ -384,11 +384,11 @@ class ProjectTask(models.Model): for task in self: task.remaining_hours_so = mapped_remaining_hours[task._origin.id] - @api.depends('analytic_account_id.active') + @api.depends('so_analytic_account_id.active') def _compute_analytic_account_active(self): super()._compute_analytic_account_active() for task in self: - task.analytic_account_active = task.analytic_account_active or task.analytic_account_id.active + task.analytic_account_active = task.analytic_account_active or task.so_analytic_account_id.active @api.depends('allow_billable') def _compute_sale_order_id(self): @@ -437,4 +437,4 @@ class ProjectTaskRecurrence(models.Model): @api.model def _get_recurring_fields(self): - return ['analytic_account_id'] + super(ProjectTaskRecurrence, self)._get_recurring_fields() + return ['so_analytic_account_id'] + super(ProjectTaskRecurrence, self)._get_recurring_fields() diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index 5221810b28a..91c69de17f3 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -115,9 +115,6 @@ - - -