[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:
committed by
Laurent Stukkens (LTU)
parent
d4a0bcaa5a
commit
dd706ea024
@@ -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'))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user