@@ -3,3 +3,4 @@
|
||||
from . import product
|
||||
from . import project
|
||||
from . import sale_order
|
||||
from . import sale_order_line
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, Command, fields, models, _
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
from odoo.tools.sql import column_exists, create_column
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
@@ -114,283 +112,3 @@ class SaleOrder(models.Model):
|
||||
if 'state' in values and values['state'] == 'cancel':
|
||||
self.project_id.sale_line_id = False
|
||||
return super(SaleOrder, self).write(values)
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
project_id = fields.Many2one(
|
||||
'project.project', 'Generated Project',
|
||||
index=True, copy=False, help="Project generated by the sales order item")
|
||||
task_id = fields.Many2one(
|
||||
'project.task', 'Generated Task',
|
||||
index=True, copy=False, help="Task generated by the sales order item")
|
||||
is_service = fields.Boolean("Is a Service", compute='_compute_is_service', store=True, compute_sudo=True, help="Sales Order item should generate a task and/or a project, depending on the product settings.")
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_is_service(self):
|
||||
for so_line in self:
|
||||
so_line.is_service = so_line.product_id.type == 'service'
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_product_updatable(self):
|
||||
for line in self:
|
||||
if line.product_id.type == 'service' and line.state == 'sale':
|
||||
line.product_updatable = False
|
||||
else:
|
||||
super(SaleOrderLine, line)._compute_product_updatable()
|
||||
|
||||
def _auto_init(self):
|
||||
"""
|
||||
Create column to stop ORM from computing it himself (too slow)
|
||||
"""
|
||||
if not column_exists(self.env.cr, 'sale_order_line', 'is_service'):
|
||||
create_column(self.env.cr, 'sale_order_line', 'is_service', 'bool')
|
||||
self.env.cr.execute("""
|
||||
UPDATE sale_order_line line
|
||||
SET is_service = (pt.type = 'service')
|
||||
FROM product_product pp
|
||||
LEFT JOIN product_template pt ON pt.id = pp.product_tmpl_id
|
||||
WHERE pp.id = line.product_id
|
||||
""")
|
||||
return super()._auto_init()
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
lines = super().create(vals_list)
|
||||
# Do not generate task/project when expense SO line, but allow
|
||||
# generate task with hours=0.
|
||||
for line in lines:
|
||||
if line.state == 'sale' and not line.is_expense:
|
||||
line.sudo()._timesheet_service_generation()
|
||||
# if the SO line created a task, post a message on the order
|
||||
if line.task_id:
|
||||
msg_body = _("Task Created (%s): <a href=# data-oe-model=project.task data-oe-id=%d>%s</a>") % (line.product_id.name, line.task_id.id, line.task_id.name)
|
||||
line.order_id.message_post(body=msg_body)
|
||||
return lines
|
||||
|
||||
def write(self, values):
|
||||
result = super().write(values)
|
||||
# changing the ordered quantity should change the planned hours on the
|
||||
# task, whatever the SO state. It will be blocked by the super in case
|
||||
# of a locked sale order.
|
||||
if 'product_uom_qty' in values and not self.env.context.get('no_update_planned_hours', False):
|
||||
for line in self:
|
||||
if line.task_id and line.product_id.type == 'service':
|
||||
planned_hours = line._convert_qty_company_hours(line.task_id.company_id)
|
||||
line.task_id.write({'planned_hours': planned_hours})
|
||||
return result
|
||||
|
||||
###########################################
|
||||
# Service : Project and task generation
|
||||
###########################################
|
||||
|
||||
def _convert_qty_company_hours(self, dest_company):
|
||||
return self.product_uom_qty
|
||||
|
||||
def _timesheet_create_project_prepare_values(self):
|
||||
"""Generate project values"""
|
||||
account = self.order_id.analytic_account_id
|
||||
if not account:
|
||||
service_products = self.order_id.order_line.product_id.filtered(lambda p: p.type == 'service' and p.default_code)
|
||||
default_code = service_products.default_code if len(service_products) == 1 else None
|
||||
self.order_id._create_analytic_account(prefix=default_code)
|
||||
account = self.order_id.analytic_account_id
|
||||
|
||||
# create the project or duplicate one
|
||||
return {
|
||||
'name': '%s - %s' % (self.order_id.client_order_ref, self.order_id.name) if self.order_id.client_order_ref else self.order_id.name,
|
||||
'analytic_account_id': account.id,
|
||||
'analytic_tag_ids': [Command.set(self.analytic_tag_ids.ids)],
|
||||
'partner_id': self.order_id.partner_id.id,
|
||||
'sale_line_id': self.id,
|
||||
'active': True,
|
||||
'company_id': self.company_id.id,
|
||||
}
|
||||
|
||||
def _timesheet_create_project(self):
|
||||
""" Generate project for the given so line, and link it.
|
||||
:param project: record of project.project in which the task should be created
|
||||
:return task: record of the created task
|
||||
"""
|
||||
self.ensure_one()
|
||||
values = self._timesheet_create_project_prepare_values()
|
||||
if self.product_id.project_template_id:
|
||||
values['name'] = "%s - %s" % (values['name'], self.product_id.project_template_id.name)
|
||||
project = self.product_id.project_template_id.copy(values)
|
||||
project.tasks.write({
|
||||
'sale_line_id': self.id,
|
||||
'partner_id': self.order_id.partner_id.id,
|
||||
'email_from': self.order_id.partner_id.email,
|
||||
})
|
||||
# duplicating a project doesn't set the SO on sub-tasks
|
||||
project.tasks.filtered(lambda task: task.parent_id != False).write({
|
||||
'sale_line_id': self.id,
|
||||
'sale_order_id': self.order_id,
|
||||
})
|
||||
else:
|
||||
project_only_sol_count = self.env['sale.order.line'].search_count([
|
||||
('order_id', '=', self.order_id.id),
|
||||
('product_id.service_tracking', 'in', ['project_only', 'task_in_project']),
|
||||
])
|
||||
if project_only_sol_count == 1:
|
||||
values['name'] = "%s - [%s] %s" % (values['name'], self.product_id.default_code, self.product_id.name) if self.product_id.default_code else "%s - %s" % (values['name'], self.product_id.name)
|
||||
project = self.env['project.project'].create(values)
|
||||
|
||||
# Avoid new tasks to go to 'Undefined Stage'
|
||||
if not project.type_ids:
|
||||
project.type_ids = self.env['project.task.type'].create({'name': _('New')})
|
||||
|
||||
# link project as generated by current so line
|
||||
self.write({'project_id': project.id})
|
||||
return project
|
||||
|
||||
def _timesheet_create_task_prepare_values(self, project):
|
||||
self.ensure_one()
|
||||
planned_hours = self._convert_qty_company_hours(self.company_id)
|
||||
sale_line_name_parts = self.name.split('\n')
|
||||
title = sale_line_name_parts[0] or self.product_id.name
|
||||
description = '<br/>'.join(sale_line_name_parts[1:])
|
||||
return {
|
||||
'name': title if project.sale_line_id else '%s - %s' % (self.order_id.name or '', title),
|
||||
'planned_hours': planned_hours,
|
||||
'partner_id': self.order_id.partner_id.id,
|
||||
'email_from': self.order_id.partner_id.email,
|
||||
'description': description,
|
||||
'project_id': project.id,
|
||||
'sale_line_id': self.id,
|
||||
'sale_order_id': self.order_id.id,
|
||||
'company_id': project.company_id.id,
|
||||
'user_ids': False, # force non assigned task, as created as sudo()
|
||||
}
|
||||
|
||||
def _timesheet_create_task(self, project):
|
||||
""" Generate task for the given so line, and link it.
|
||||
:param project: record of project.project in which the task should be created
|
||||
:return task: record of the created task
|
||||
"""
|
||||
values = self._timesheet_create_task_prepare_values(project)
|
||||
task = self.env['project.task'].sudo().create(values)
|
||||
self.write({'task_id': task.id})
|
||||
# post message on task
|
||||
task_msg = _("This task has been created from: <a href=# data-oe-model=sale.order data-oe-id=%d>%s</a> (%s)") % (self.order_id.id, self.order_id.name, self.product_id.name)
|
||||
task.message_post(body=task_msg)
|
||||
return task
|
||||
|
||||
def _timesheet_service_generation(self):
|
||||
""" For service lines, create the task or the project. If already exists, it simply links
|
||||
the existing one to the line.
|
||||
Note: If the SO was confirmed, cancelled, set to draft then confirmed, avoid creating a
|
||||
new project/task. This explains the searches on 'sale_line_id' on project/task. This also
|
||||
implied if so line of generated task has been modified, we may regenerate it.
|
||||
"""
|
||||
so_line_task_global_project = self.filtered(lambda sol: sol.is_service and sol.product_id.service_tracking == 'task_global_project')
|
||||
so_line_new_project = self.filtered(lambda sol: sol.is_service and sol.product_id.service_tracking in ['project_only', 'task_in_project'])
|
||||
|
||||
# search so lines from SO of current so lines having their project generated, in order to check if the current one can
|
||||
# create its own project, or reuse the one of its order.
|
||||
map_so_project = {}
|
||||
if so_line_new_project:
|
||||
order_ids = self.mapped('order_id').ids
|
||||
so_lines_with_project = self.search([('order_id', 'in', order_ids), ('project_id', '!=', False), ('product_id.service_tracking', 'in', ['project_only', 'task_in_project']), ('product_id.project_template_id', '=', False)])
|
||||
map_so_project = {sol.order_id.id: sol.project_id for sol in so_lines_with_project}
|
||||
so_lines_with_project_templates = self.search([('order_id', 'in', order_ids), ('project_id', '!=', False), ('product_id.service_tracking', 'in', ['project_only', 'task_in_project']), ('product_id.project_template_id', '!=', False)])
|
||||
map_so_project_templates = {(sol.order_id.id, sol.product_id.project_template_id.id): sol.project_id for sol in so_lines_with_project_templates}
|
||||
|
||||
# search the global project of current SO lines, in which create their task
|
||||
map_sol_project = {}
|
||||
if so_line_task_global_project:
|
||||
map_sol_project = {sol.id: sol.product_id.with_company(sol.company_id).project_id for sol in so_line_task_global_project}
|
||||
|
||||
def _can_create_project(sol):
|
||||
if not sol.project_id:
|
||||
if sol.product_id.project_template_id:
|
||||
return (sol.order_id.id, sol.product_id.project_template_id.id) not in map_so_project_templates
|
||||
elif sol.order_id.id not in map_so_project:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _determine_project(so_line):
|
||||
"""Determine the project for this sale order line.
|
||||
Rules are different based on the service_tracking:
|
||||
|
||||
- 'project_only': the project_id can only come from the sale order line itself
|
||||
- 'task_in_project': the project_id comes from the sale order line only if no project_id was configured
|
||||
on the parent sale order"""
|
||||
|
||||
if so_line.product_id.service_tracking == 'project_only':
|
||||
return so_line.project_id
|
||||
elif so_line.product_id.service_tracking == 'task_in_project':
|
||||
return so_line.order_id.project_id or so_line.project_id
|
||||
|
||||
return False
|
||||
|
||||
# task_global_project: create task in global project
|
||||
for so_line in so_line_task_global_project:
|
||||
if not so_line.task_id:
|
||||
if map_sol_project.get(so_line.id) and so_line.product_uom_qty > 0:
|
||||
so_line._timesheet_create_task(project=map_sol_project[so_line.id])
|
||||
|
||||
# project_only, task_in_project: create a new project, based or not on a template (1 per SO). May be create a task too.
|
||||
# if 'task_in_project' and project_id configured on SO, use that one instead
|
||||
for so_line in so_line_new_project:
|
||||
project = _determine_project(so_line)
|
||||
if not project and _can_create_project(so_line):
|
||||
project = so_line._timesheet_create_project()
|
||||
if so_line.product_id.project_template_id:
|
||||
map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)] = project
|
||||
else:
|
||||
map_so_project[so_line.order_id.id] = project
|
||||
elif not project:
|
||||
# Attach subsequent SO lines to the created project
|
||||
so_line.project_id = (
|
||||
map_so_project_templates.get((so_line.order_id.id, so_line.product_id.project_template_id.id))
|
||||
or map_so_project.get(so_line.order_id.id)
|
||||
)
|
||||
if so_line.product_id.service_tracking == 'task_in_project':
|
||||
if not project:
|
||||
if so_line.product_id.project_template_id:
|
||||
project = map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)]
|
||||
else:
|
||||
project = map_so_project[so_line.order_id.id]
|
||||
if not so_line.task_id:
|
||||
so_line._timesheet_create_task(project=project)
|
||||
|
||||
def _prepare_invoice_line(self, **optional_values):
|
||||
"""
|
||||
If the sale order line isn't linked to a sale order which already have a default analytic account,
|
||||
this method allows to retrieve the analytic account which is linked to project or task directly linked
|
||||
to this sale order line, or the analytic account of the project which uses this sale order line, if it exists.
|
||||
"""
|
||||
values = super(SaleOrderLine, self)._prepare_invoice_line(**optional_values)
|
||||
if not values.get('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:
|
||||
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'])
|
||||
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]
|
||||
if self.task_id.analytic_tag_ids or self.task_id.project_id.analytic_tag_ids:
|
||||
values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in self.task_id.analytic_tag_ids | self.task_id.project_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), ('project_ids.sale_line_id', '=', self.id)
|
||||
])
|
||||
values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in tag_ids]
|
||||
return values
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, Command, fields, models, _
|
||||
from odoo.tools.sql import column_exists, create_column
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
project_id = fields.Many2one(
|
||||
'project.project', 'Generated Project',
|
||||
index=True, copy=False, help="Project generated by the sales order item")
|
||||
task_id = fields.Many2one(
|
||||
'project.task', 'Generated Task',
|
||||
index=True, copy=False, help="Task generated by the sales order item")
|
||||
is_service = fields.Boolean("Is a Service", compute='_compute_is_service', store=True, compute_sudo=True, help="Sales Order item should generate a task and/or a project, depending on the product settings.")
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_is_service(self):
|
||||
for so_line in self:
|
||||
so_line.is_service = so_line.product_id.type == 'service'
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_product_updatable(self):
|
||||
for line in self:
|
||||
if line.product_id.type == 'service' and line.state == 'sale':
|
||||
line.product_updatable = False
|
||||
else:
|
||||
super(SaleOrderLine, line)._compute_product_updatable()
|
||||
|
||||
def _auto_init(self):
|
||||
"""
|
||||
Create column to stop ORM from computing it himself (too slow)
|
||||
"""
|
||||
if not column_exists(self.env.cr, 'sale_order_line', 'is_service'):
|
||||
create_column(self.env.cr, 'sale_order_line', 'is_service', 'bool')
|
||||
self.env.cr.execute("""
|
||||
UPDATE sale_order_line line
|
||||
SET is_service = (pt.type = 'service')
|
||||
FROM product_product pp
|
||||
LEFT JOIN product_template pt ON pt.id = pp.product_tmpl_id
|
||||
WHERE pp.id = line.product_id
|
||||
""")
|
||||
return super()._auto_init()
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
lines = super().create(vals_list)
|
||||
# Do not generate task/project when expense SO line, but allow
|
||||
# generate task with hours=0.
|
||||
for line in lines:
|
||||
if line.state == 'sale' and not line.is_expense:
|
||||
line.sudo()._timesheet_service_generation()
|
||||
# if the SO line created a task, post a message on the order
|
||||
if line.task_id:
|
||||
msg_body = _("Task Created (%s): <a href=# data-oe-model=project.task data-oe-id=%d>%s</a>") % (line.product_id.name, line.task_id.id, line.task_id.name)
|
||||
line.order_id.message_post(body=msg_body)
|
||||
return lines
|
||||
|
||||
def write(self, values):
|
||||
result = super().write(values)
|
||||
# changing the ordered quantity should change the planned hours on the
|
||||
# task, whatever the SO state. It will be blocked by the super in case
|
||||
# of a locked sale order.
|
||||
if 'product_uom_qty' in values and not self.env.context.get('no_update_planned_hours', False):
|
||||
for line in self:
|
||||
if line.task_id and line.product_id.type == 'service':
|
||||
planned_hours = line._convert_qty_company_hours(line.task_id.company_id)
|
||||
line.task_id.write({'planned_hours': planned_hours})
|
||||
return result
|
||||
|
||||
###########################################
|
||||
# Service : Project and task generation
|
||||
###########################################
|
||||
|
||||
def _convert_qty_company_hours(self, dest_company):
|
||||
return self.product_uom_qty
|
||||
|
||||
def _timesheet_create_project_prepare_values(self):
|
||||
"""Generate project values"""
|
||||
account = self.order_id.analytic_account_id
|
||||
if not account:
|
||||
service_products = self.order_id.order_line.product_id.filtered(lambda p: p.type == 'service' and p.default_code)
|
||||
default_code = service_products.default_code if len(service_products) == 1 else None
|
||||
self.order_id._create_analytic_account(prefix=default_code)
|
||||
account = self.order_id.analytic_account_id
|
||||
|
||||
# create the project or duplicate one
|
||||
return {
|
||||
'name': '%s - %s' % (self.order_id.client_order_ref, self.order_id.name) if self.order_id.client_order_ref else self.order_id.name,
|
||||
'analytic_account_id': account.id,
|
||||
'analytic_tag_ids': [Command.set(self.analytic_tag_ids.ids)],
|
||||
'partner_id': self.order_id.partner_id.id,
|
||||
'sale_line_id': self.id,
|
||||
'active': True,
|
||||
'company_id': self.company_id.id,
|
||||
}
|
||||
|
||||
def _timesheet_create_project(self):
|
||||
""" Generate project for the given so line, and link it.
|
||||
:param project: record of project.project in which the task should be created
|
||||
:return task: record of the created task
|
||||
"""
|
||||
self.ensure_one()
|
||||
values = self._timesheet_create_project_prepare_values()
|
||||
if self.product_id.project_template_id:
|
||||
values['name'] = "%s - %s" % (values['name'], self.product_id.project_template_id.name)
|
||||
project = self.product_id.project_template_id.copy(values)
|
||||
project.tasks.write({
|
||||
'sale_line_id': self.id,
|
||||
'partner_id': self.order_id.partner_id.id,
|
||||
'email_from': self.order_id.partner_id.email,
|
||||
})
|
||||
# duplicating a project doesn't set the SO on sub-tasks
|
||||
project.tasks.filtered(lambda task: task.parent_id != False).write({
|
||||
'sale_line_id': self.id,
|
||||
'sale_order_id': self.order_id,
|
||||
})
|
||||
else:
|
||||
project_only_sol_count = self.env['sale.order.line'].search_count([
|
||||
('order_id', '=', self.order_id.id),
|
||||
('product_id.service_tracking', 'in', ['project_only', 'task_in_project']),
|
||||
])
|
||||
if project_only_sol_count == 1:
|
||||
values['name'] = "%s - [%s] %s" % (values['name'], self.product_id.default_code, self.product_id.name) if self.product_id.default_code else "%s - %s" % (values['name'], self.product_id.name)
|
||||
project = self.env['project.project'].create(values)
|
||||
|
||||
# Avoid new tasks to go to 'Undefined Stage'
|
||||
if not project.type_ids:
|
||||
project.type_ids = self.env['project.task.type'].create({'name': _('New')})
|
||||
|
||||
# link project as generated by current so line
|
||||
self.write({'project_id': project.id})
|
||||
return project
|
||||
|
||||
def _timesheet_create_task_prepare_values(self, project):
|
||||
self.ensure_one()
|
||||
planned_hours = self._convert_qty_company_hours(self.company_id)
|
||||
sale_line_name_parts = self.name.split('\n')
|
||||
title = sale_line_name_parts[0] or self.product_id.name
|
||||
description = '<br/>'.join(sale_line_name_parts[1:])
|
||||
return {
|
||||
'name': title if project.sale_line_id else '%s - %s' % (self.order_id.name or '', title),
|
||||
'planned_hours': planned_hours,
|
||||
'partner_id': self.order_id.partner_id.id,
|
||||
'email_from': self.order_id.partner_id.email,
|
||||
'description': description,
|
||||
'project_id': project.id,
|
||||
'sale_line_id': self.id,
|
||||
'sale_order_id': self.order_id.id,
|
||||
'company_id': project.company_id.id,
|
||||
'user_ids': False, # force non assigned task, as created as sudo()
|
||||
}
|
||||
|
||||
def _timesheet_create_task(self, project):
|
||||
""" Generate task for the given so line, and link it.
|
||||
:param project: record of project.project in which the task should be created
|
||||
:return task: record of the created task
|
||||
"""
|
||||
values = self._timesheet_create_task_prepare_values(project)
|
||||
task = self.env['project.task'].sudo().create(values)
|
||||
self.write({'task_id': task.id})
|
||||
# post message on task
|
||||
task_msg = _("This task has been created from: <a href=# data-oe-model=sale.order data-oe-id=%d>%s</a> (%s)") % (self.order_id.id, self.order_id.name, self.product_id.name)
|
||||
task.message_post(body=task_msg)
|
||||
return task
|
||||
|
||||
def _timesheet_service_generation(self):
|
||||
""" For service lines, create the task or the project. If already exists, it simply links
|
||||
the existing one to the line.
|
||||
Note: If the SO was confirmed, cancelled, set to draft then confirmed, avoid creating a
|
||||
new project/task. This explains the searches on 'sale_line_id' on project/task. This also
|
||||
implied if so line of generated task has been modified, we may regenerate it.
|
||||
"""
|
||||
so_line_task_global_project = self.filtered(lambda sol: sol.is_service and sol.product_id.service_tracking == 'task_global_project')
|
||||
so_line_new_project = self.filtered(lambda sol: sol.is_service and sol.product_id.service_tracking in ['project_only', 'task_in_project'])
|
||||
|
||||
# search so lines from SO of current so lines having their project generated, in order to check if the current one can
|
||||
# create its own project, or reuse the one of its order.
|
||||
map_so_project = {}
|
||||
if so_line_new_project:
|
||||
order_ids = self.mapped('order_id').ids
|
||||
so_lines_with_project = self.search([('order_id', 'in', order_ids), ('project_id', '!=', False), ('product_id.service_tracking', 'in', ['project_only', 'task_in_project']), ('product_id.project_template_id', '=', False)])
|
||||
map_so_project = {sol.order_id.id: sol.project_id for sol in so_lines_with_project}
|
||||
so_lines_with_project_templates = self.search([('order_id', 'in', order_ids), ('project_id', '!=', False), ('product_id.service_tracking', 'in', ['project_only', 'task_in_project']), ('product_id.project_template_id', '!=', False)])
|
||||
map_so_project_templates = {(sol.order_id.id, sol.product_id.project_template_id.id): sol.project_id for sol in so_lines_with_project_templates}
|
||||
|
||||
# search the global project of current SO lines, in which create their task
|
||||
map_sol_project = {}
|
||||
if so_line_task_global_project:
|
||||
map_sol_project = {sol.id: sol.product_id.with_company(sol.company_id).project_id for sol in so_line_task_global_project}
|
||||
|
||||
def _can_create_project(sol):
|
||||
if not sol.project_id:
|
||||
if sol.product_id.project_template_id:
|
||||
return (sol.order_id.id, sol.product_id.project_template_id.id) not in map_so_project_templates
|
||||
elif sol.order_id.id not in map_so_project:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _determine_project(so_line):
|
||||
"""Determine the project for this sale order line.
|
||||
Rules are different based on the service_tracking:
|
||||
|
||||
- 'project_only': the project_id can only come from the sale order line itself
|
||||
- 'task_in_project': the project_id comes from the sale order line only if no project_id was configured
|
||||
on the parent sale order"""
|
||||
|
||||
if so_line.product_id.service_tracking == 'project_only':
|
||||
return so_line.project_id
|
||||
elif so_line.product_id.service_tracking == 'task_in_project':
|
||||
return so_line.order_id.project_id or so_line.project_id
|
||||
|
||||
return False
|
||||
|
||||
# task_global_project: create task in global project
|
||||
for so_line in so_line_task_global_project:
|
||||
if not so_line.task_id:
|
||||
if map_sol_project.get(so_line.id) and so_line.product_uom_qty > 0:
|
||||
so_line._timesheet_create_task(project=map_sol_project[so_line.id])
|
||||
|
||||
# project_only, task_in_project: create a new project, based or not on a template (1 per SO). May be create a task too.
|
||||
# if 'task_in_project' and project_id configured on SO, use that one instead
|
||||
for so_line in so_line_new_project:
|
||||
project = _determine_project(so_line)
|
||||
if not project and _can_create_project(so_line):
|
||||
project = so_line._timesheet_create_project()
|
||||
if so_line.product_id.project_template_id:
|
||||
map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)] = project
|
||||
else:
|
||||
map_so_project[so_line.order_id.id] = project
|
||||
elif not project:
|
||||
# Attach subsequent SO lines to the created project
|
||||
so_line.project_id = (
|
||||
map_so_project_templates.get((so_line.order_id.id, so_line.product_id.project_template_id.id))
|
||||
or map_so_project.get(so_line.order_id.id)
|
||||
)
|
||||
if so_line.product_id.service_tracking == 'task_in_project':
|
||||
if not project:
|
||||
if so_line.product_id.project_template_id:
|
||||
project = map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)]
|
||||
else:
|
||||
project = map_so_project[so_line.order_id.id]
|
||||
if not so_line.task_id:
|
||||
so_line._timesheet_create_task(project=project)
|
||||
|
||||
def _prepare_invoice_line(self, **optional_values):
|
||||
"""
|
||||
If the sale order line isn't linked to a sale order which already have a default analytic account,
|
||||
this method allows to retrieve the analytic account which is linked to project or task directly linked
|
||||
to this sale order line, or the analytic account of the project which uses this sale order line, if it exists.
|
||||
"""
|
||||
values = super(SaleOrderLine, self)._prepare_invoice_line(**optional_values)
|
||||
if not values.get('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:
|
||||
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'])
|
||||
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]
|
||||
if self.task_id.analytic_tag_ids or self.task_id.project_id.analytic_tag_ids:
|
||||
values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in self.task_id.analytic_tag_ids | self.task_id.project_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), ('project_ids.sale_line_id', '=', self.id)
|
||||
])
|
||||
values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in tag_ids]
|
||||
return values
|
||||
Reference in New Issue
Block a user