Skip to content

Commit

Permalink
[ADD] sale_project: Move task generation from SO feature
Browse files Browse the repository at this point in the history
Purpose
=======

If the user doesn't have the timesheets app, it is not possible to generate
a task from an SOL. However, the user may want to generate a task without
necessarily timesheeting on it (e.g. a task in Field Service).

Specification
=============

Move the feature to a new module sale_project, sale_timesheet becomes
dependent on it.

Task-2123707

closes #40594

Closes: #40594
Related: odoo/enterprise#6851
Signed-off-by: Yannick Tivisse (yti) <[email protected]>
  • Loading branch information
mba-odoo authored and tivisse committed Dec 27, 2019
1 parent de242ca commit 447b33a
Show file tree
Hide file tree
Showing 24 changed files with 774 additions and 498 deletions.
4 changes: 4 additions & 0 deletions addons/sale_project/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models
21 changes: 21 additions & 0 deletions addons/sale_project/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "Sales - Project",
'summary': "Task Generation from Sales Orders",
'description': """
Allows to create task from your sales order
=============================================
This module allows to generate a project/task from sales orders.
""",
'category': 'Hidden',
'depends': ['sale_management', 'project'],
'data': [
'security/ir.model.access.csv',
'security/sale_project_security.xml',
'views/product_views.xml',
'views/project_task_views.xml',
'views/sale_order_views.xml',
],
'auto_install': True,
}
5 changes: 5 additions & 0 deletions addons/sale_project/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-

from . import product
from . import project
from . import sale_order
64 changes: 64 additions & 0 deletions addons/sale_project/models/product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, fields, models, _
from odoo.exceptions import ValidationError


class ProductTemplate(models.Model):
_inherit = 'product.template'

service_tracking = fields.Selection([
('no', 'Don\'t create task'),
('task_global_project', 'Create a task in an existing project'),
('task_in_project', 'Create a task in sales order\'s project'),
('project_only', 'Create a new project but no task')],
string="Service Tracking", default="no",
help="On Sales order confirmation, this product can generate a project and/or task. \
From those, you can track the service you are selling.\n \
'In sale order\'s project': Will use the sale order\'s configured project if defined or fallback to \
creating a new project based on the selected template.")
project_id = fields.Many2one(
'project.project', 'Project', company_dependent=True,
help='Select a non billable project on which tasks can be created. This setting must be set for each company.')
project_template_id = fields.Many2one(
'project.project', 'Project Template', company_dependent=True, copy=True,
help='Select a non billable project to be the skeleton of the new created project when selling the current product. Its stages and tasks will be duplicated.')

@api.constrains('project_id', 'project_template_id')
def _check_project_and_template(self):
""" NOTE 'service_tracking' should be in decorator parameters but since ORM check constraints twice (one after setting
stored fields, one after setting non stored field), the error is raised when company-dependent fields are not set.
So, this constraints does cover all cases and inconsistent can still be recorded until the ORM change its behavior.
"""
for product in self:
if product.service_tracking == 'no' and (product.project_id or product.project_template_id):
raise ValidationError(_('The product %s should not have a project nor a project template since it will not generate project.') % (product.name,))
elif product.service_tracking == 'task_global_project' and product.project_template_id:
raise ValidationError(_('The product %s should not have a project template since it will generate a task in a global project.') % (product.name,))
elif product.service_tracking in ['task_in_project', 'project_only'] and product.project_id:
raise ValidationError(_('The product %s should not have a global project since it will generate a project.') % (product.name,))

@api.onchange('service_tracking')
def _onchange_service_tracking(self):
if self.service_tracking == 'no':
self.project_id = False
self.project_template_id = False
elif self.service_tracking == 'task_global_project':
self.project_template_id = False
elif self.service_tracking in ['task_in_project', 'project_only']:
self.project_id = False


class ProductProduct(models.Model):
_inherit = 'product.product'

@api.onchange('service_tracking')
def _onchange_service_tracking(self):
if self.service_tracking == 'no':
self.project_id = False
self.project_template_id = False
elif self.service_tracking == 'task_global_project':
self.project_template_id = False
elif self.service_tracking in ['task_in_project', 'project_only']:
self.project_id = False
88 changes: 88 additions & 0 deletions addons/sale_project/models/project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, fields, models, _
from odoo.exceptions import ValidationError


class Project(models.Model):
_inherit = 'project.project'

sale_line_id = fields.Many2one(
'sale.order.line', 'Sales Order Item', copy=False,
domain="[('is_expense', '=', False), ('order_id', '=', sale_order_id), ('state', 'in', ['sale', 'done']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
help="Sales order item to which the project is linked. If an employee timesheets on a task that does not have a "
"sale order item defines, and if this employee is not in the 'Employee/Sales Order Item Mapping' of the project, "
"the timesheet entry will be linked to the sales order item defined on the project.")
sale_order_id = fields.Many2one('sale.order', 'Sales Order', domain="[('partner_id', '=', partner_id)]", readonly=True, copy=False, help="Sales order to which the project is linked.")

_sql_constraints = [
('sale_order_required_if_sale_line', "CHECK((sale_line_id IS NOT NULL AND sale_order_id IS NOT NULL) OR (sale_line_id IS NULL))", 'The Project should be linked to a Sale Order to select an Sale Order Items.'),
]

@api.model
def _map_tasks_default_valeus(self, task, project):
defaults = super()._map_tasks_default_valeus(task, project)
defaults['sale_line_id'] = False
return defaults


class ProjectTask(models.Model):
_inherit = "project.task"

sale_order_id = fields.Many2one('sale.order', 'Sales Order', help="Sales order to which the task is linked.")
sale_line_id = fields.Many2one(
'sale.order.line', 'Sales Order Item', domain="[('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_id', '=?', project_sale_order_id)]",
compute='_compute_sale_line', store=True, readonly=False, copy=False,
help="Sales order item to which the task is linked. If an employee timesheets on a this task, "
"and if this employee is not in the 'Employee/Sales Order Item Mapping' of the project, the "
"timesheet entry will be linked to this sales order item.")
project_sale_order_id = fields.Many2one('sale.order', string="project's sale order", related='project_id.sale_order_id')

@api.depends('project_id.sale_line_id.order_partner_id')
def _compute_partner_id(self):
for task in self:
if not task.partner_id:
task.partner_id = task.project_id.sale_line_id.order_partner_id
super()._compute_partner_id()

@api.depends('partner_id.commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id')
def _compute_sale_line(self):
for task in self:
if not task.sale_line_id:
task.sale_line_id = task.parent_id.sale_line_id or task.project_id.sale_line_id
# check sale_line_id and customer are coherent
if task.sale_line_id.order_partner_id.commercial_partner_id != task.partner_id.commercial_partner_id:
task.sale_line_id = False

@api.constrains('sale_line_id')
def _check_sale_line_type(self):
for task in self.sudo():
if task.sale_line_id:
if not task.sale_line_id.is_service or task.sale_line_id.is_expense:
raise ValidationError(_('You cannot link the order item %s - %s to this task because it is a re-invoiced expense.' % (task.sale_line_id.order_id.id, task.sale_line_id.product_id.name)))

def unlink(self):
if any(task.sale_line_id for task in self):
raise ValidationError(_('You have to unlink the task from the sale order item in order to delete it.'))
return super().unlink()

# ---------------------------------------------------
# Actions
# ---------------------------------------------------

def action_view_so(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"res_model": "sale.order",
"views": [[False, "form"]],
"res_id": self.sale_order_id.id,
"context": {"create": False, "show_sale": True},
}

def rating_get_partner_id(self):
partner = self.partner_id or self.sale_line_id.order_id.partner_id
if partner:
return partner
return super().rating_get_partner_id()
Loading

0 comments on commit 447b33a

Please sign in to comment.