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