[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
This commit is contained in:
Thibault Libioulle
2021-09-03 12:45:58 +02:00
committed by Laurent Stukkens (LTU)
parent d4a0bcaa5a
commit dd706ea024
10 changed files with 73 additions and 19 deletions
+13 -1
View File
@@ -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'))
+2 -2
View File
@@ -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):
+1
View File
@@ -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
@@ -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')
+17
View File
@@ -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"
@@ -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()
+2
View File
@@ -930,6 +930,8 @@
<page name="extra_info" string="Extra Info" groups="base.group_no_one">
<group>
<group>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting" context="{'default_partner_id': partner_id}"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
<field name="sequence" groups="base.group_no_one"/>
<field name="email_from" invisible="1"/>
<field name="email_cc" groups="base.group_no_one"/>
+24 -8
View File
@@ -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
+4 -4
View File
@@ -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()
@@ -115,9 +115,6 @@
<xpath expr="//header" position='inside'>
<field name="allow_billable" invisible="1"/>
</xpath>
<xpath expr="//group/field[@name='sequence']" position="before">
<field name="analytic_account_id" groups="base.group_no_one"/>
</xpath>
</field>
</record>