diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py
index 4a7a58f05e6..7cdfc9817cf 100644
--- a/addons/sale/models/product_template.py
+++ b/addons/sale/models/product_template.py
@@ -8,11 +8,11 @@ from odoo.addons.base.res.res_partner import WARNING_MESSAGE, WARNING_HELP
class ProductTemplate(models.Model):
_inherit = 'product.template'
- track_service = fields.Selection([('manual', 'Manually set quantities on order')], string='Track Service',
+ service_type = fields.Selection([('manual', 'Manually set quantities on order')], string='Track Service',
help="Manually set quantities on order: Invoice based on the manually entered quantity, without creating an analytic account.\n"
"Timesheets on contract: Invoice based on the tracked hours on the related timesheet.\n"
"Create a task and track hours: Create a task on the sales order validation and track the work hours.",
- default='manual')
+ default='manual', oldname='track_service')
sale_line_warn = fields.Selection(WARNING_MESSAGE, 'Sales Order Line', help=WARNING_HELP, required=True, default="no-message")
sale_line_warn_msg = fields.Text('Message for Sales Order Line')
expense_policy = fields.Selection(
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 31d6e2053e4..cc5d909db85 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -659,7 +659,7 @@ class SaleOrderLine(models.Model):
@api.depends('product_id.invoice_policy', 'order_id.state')
def _compute_qty_delivered_updateable(self):
for line in self:
- line.qty_delivered_updateable = (line.order_id.state == 'sale') and (line.product_id.track_service == 'manual') and (line.product_id.expense_policy == 'no')
+ line.qty_delivered_updateable = (line.order_id.state == 'sale') and (line.product_id.service_type == 'manual') and (line.product_id.expense_policy == 'no')
@api.depends('invoice_lines',
'invoice_lines.price_total',
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index 576c37b923f..cded496290b 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -696,7 +696,7 @@
-
+
diff --git a/addons/sale_expense/views/product_view.xml b/addons/sale_expense/views/product_view.xml
index 90539c2fade..2f27621a522 100644
--- a/addons/sale_expense/views/product_view.xml
+++ b/addons/sale_expense/views/product_view.xml
@@ -5,7 +5,7 @@
product.template
-
+
diff --git a/addons/sale_timesheet/data/sale_service_demo.xml b/addons/sale_timesheet/data/sale_service_demo.xml
index cfddb602bc8..96aadd83975 100644
--- a/addons/sale_timesheet/data/sale_service_demo.xml
+++ b/addons/sale_timesheet/data/sale_service_demo.xml
@@ -2,11 +2,13 @@
- timesheet
+ timesheet
+ project_only
Support Contract (on timesheet)
- timesheet
+ timesheet
+ project_only
@@ -27,7 +29,8 @@
- task
+ timesheet
+ task_global_project
diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py
index 064559515cd..d6da4910a36 100644
--- a/addons/sale_timesheet/models/account.py
+++ b/addons/sale_timesheet/models/account.py
@@ -41,33 +41,6 @@ class AccountAnalyticLine(models.Model):
return True
- @api.multi
- def _sale_postprocess(self, additionnal_so_lines=None):
- """ Set the SO line before recomputed delivered quantity on SO line.
- Only applied for timesheet lines, having no SO line set yet.
- """
- self.filtered(lambda aal: not aal.so_line and aal.project_id)._timesheet_set_sale_order_line()
- return super(AccountAnalyticLine, self)._sale_postprocess(additionnal_so_lines=additionnal_so_lines)
-
- @api.multi
- def _timesheet_set_sale_order_line(self):
- """ Automatically set the SO line on the analytic line, for the timesheet flow. It retrives
- the SO line which has create the project (track_service='timesheet'). There is only one line
- per SO like this (See constraint).
- """
- for timesheet in self.filtered(lambda aal: not aal.so_line):
- sol = self.env['sale.order.line'].search([
- ('order_id.analytic_account_id', '=', timesheet.account_id.id),
- ('state', 'in', ('sale', 'done')),
- ('product_id.track_service', '=', 'timesheet'),
- ('product_id.type', '=', 'service')
- ], limit=1)
- if sol:
- timesheet.write({
- 'so_line': sol.id,
- 'product_id': sol.product_id.id,
- })
-
def _get_timesheet_values(self, values):
result = {}
values = values if values is not None else {}
@@ -144,7 +117,7 @@ class AccountAnalyticLine(models.Model):
if so_line.product_id.invoice_policy == 'delivery':
revenue = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100)))
billable_type = 'billable_time'
- elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.track_service == 'task':
+ elif so_line.product_id.invoice_policy == 'order':
quantity_hour = unit_amount
if so_line.product_uom.category_id == timesheet_uom.category_id:
quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom)
diff --git a/addons/sale_timesheet/models/product.py b/addons/sale_timesheet/models/product.py
index 2f3c86d562f..af71631227f 100644
--- a/addons/sale_timesheet/models/product.py
+++ b/addons/sale_timesheet/models/product.py
@@ -7,16 +7,43 @@ from odoo import api, fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
- track_service = fields.Selection(selection_add=[
+ service_policy = fields.Selection([
+ ('ordered_timesheet', 'Ordered quantities'),
+ ('delivered_timesheet', 'Timesheets on tasks'),
+ ('delivered_manual', 'Milestones (manually set quantities on order)')
+ ], string="Invoice based on", compute='_compute_service_policy', inverse='_inverse_service_policy')
+ service_type = fields.Selection(selection_add=[
('timesheet', 'Timesheets on project (one fare per SO/Project)'),
- ('task', 'Create a task per order line to track hours')])
+ ])
+ service_tracking = fields.Selection([
+ ('no', 'Don\'t create task'),
+ ('task_global_project', 'Create a task in an existing project'),
+ ('task_new_project', 'Create a task in a new project'),
+ ('project_only', 'Create a new project but no task'),
+ ], string="Service Tracking", default="no", help="On Sales order confirmation, this product can generate project and/or task. From thoses, you can track the service you are selling.")
project_id = fields.Many2one(
- 'project.project', 'Project', company_dependent=True,
- help='Create a task under this project on sales order validation. This setting must be set for each company.')
+ 'project.project', 'Project', company_dependent=True, domain=[('sale_line_id', '=', False)],
+ help='Select a non billable project on which tasks can be created. This setting must be set for each company.')
- @api.onchange('type')
- def _onchange_type(self):
- if self.type == 'service':
- self.track_service = 'timesheet'
- else:
- self.track_service = 'manual'
+ @api.depends('invoice_policy', 'service_type')
+ def _compute_service_policy(self):
+ for product in self:
+ policy = 'ordered_timesheet'
+ if product.invoice_policy == 'delivery':
+ policy = 'delivered_manual' if product.service_type == 'manual' else 'delivered_timesheet'
+ product.service_policy = policy
+
+ def _inverse_service_policy(self):
+ for product in self:
+ policy = product.service_policy
+ if product.service_policy == 'ordered_timesheet':
+ product.invoice_policy = 'order'
+ product.service_type = 'timesheet'
+ else:
+ product.invoice_policy = 'delivery'
+ product.service_type = 'manual' if policy == 'delivered_manual' else 'timesheet'
+
+ @api.onchange('service_tracking')
+ def _onchange_service_tracking(self):
+ if self.service_tracking != 'task_global_project':
+ self.project_id = False
diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py
index 214cab4cf39..8adeec58b06 100644
--- a/addons/sale_timesheet/models/project.py
+++ b/addons/sale_timesheet/models/project.py
@@ -8,6 +8,8 @@ from odoo.exceptions import ValidationError
class Project(models.Model):
_inherit = 'project.project'
+ sale_line_id = fields.Many2one('sale.order.line', 'Sales Order Line', readonly=True, help="Sale order line from which the project has been created. Used for tracability.")
+
@api.multi
def action_view_timesheet(self):
self.ensure_one()
diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py
index 92e154f7d78..d063126a67f 100644
--- a/addons/sale_timesheet/models/sale_order.py
+++ b/addons/sale_timesheet/models/sale_order.py
@@ -45,16 +45,13 @@ class SaleOrder(models.Model):
order.project_project_id = self.env['project.project'].search([('analytic_account_id', '=', order.analytic_account_id.id)])
@api.multi
- @api.constrains('order_line')
- def _check_multi_timesheet(self):
+ @api.depends('order_line.product_id', 'project_project_id')
+ def _compute_project_ids(self):
for order in self:
- count = 0
- for line in order.order_line:
- if line.product_id.track_service == 'timesheet':
- count += 1
- if count > 1:
- raise ValidationError(_("You can use only one product on timesheet within the same sales order. You should split your order to include only one contract based on time and material."))
- return {}
+ projects = order.order_line.mapped('product_id.project_id')
+ if order.project_project_id:
+ projects |= order.project_project_id
+ order.project_ids = projects
@api.multi
def action_confirm(self):
@@ -130,9 +127,10 @@ class SaleOrderLine(models.Model):
is_service = fields.Boolean("Is service", compute='_compute_is_service', help="Sales Order item should generate a task and/or a project, depending on the product settings.")
@api.multi
+ @api.depends('product_id.type')
def _compute_is_service(self):
for so_line in self:
- so_line.is_service = so_line.product_id.type == 'service' and so_line.product_id.track_service in ['task', 'timesheet']
+ so_line.is_service = so_line.product_id.type == 'service'
@api.model
def create(self, values):
@@ -181,8 +179,14 @@ class SaleOrderLine(models.Model):
account = self.order_id.analytic_account_id
project = Project.search([('analytic_account_id', '=', account.id)], limit=1)
if not project:
- project_id = account.sudo().project_create({'name': account.name})
+ project_id = account.sudo().project_create({
+ 'name': account.name,
+ 'allow_timesheets': self.product_id.service_type == 'timesheet',
+ })
project = Project.sudo().browse(project_id)
+ # set the SO line origin if product should create project
+ if not project.sale_line_id and self.product_id.service_tracking in ['task_new_project', 'project_only']:
+ project.write({'sale_line_id': self.id})
return project
def _timesheet_create_task_prepare_values(self):
@@ -251,7 +255,12 @@ class SaleOrderLine(models.Model):
the existing one to the line.
"""
for so_line in self.filtered(lambda sol: sol.is_service):
- if so_line.product_id.track_service == 'task':
+ # create task
+ if so_line.product_id.service_tracking == 'task_global_project':
so_line._timesheet_find_task()
- if so_line.product_id.track_service == 'timesheet':
+ # create project
+ if so_line.product_id.service_tracking == 'project_only':
so_line._timesheet_find_project()
+ # create project and task
+ if so_line.product_id.service_tracking == 'task_new_project':
+ so_line._timesheet_find_task()
diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py
index 755a4946636..c2f6f34c495 100644
--- a/addons/sale_timesheet/tests/test_sale_service.py
+++ b/addons/sale_timesheet/tests/test_sale_service.py
@@ -49,7 +49,8 @@ class TestSaleService(TestSale):
'uom_id': self.env.ref('product.product_uom_hour').id,
'uom_po_id': self.env.ref('product.product_uom_hour').id,
'default_code': 'SERV-DELI',
- 'track_service': 'task',
+ 'service_type': 'timesheet',
+ 'service_tracking': 'task_global_project',
'project_id': project.id
})
diff --git a/addons/sale_timesheet/tests/test_timesheet_revenue.py b/addons/sale_timesheet/tests/test_timesheet_revenue.py
index 19dd8a6f3c8..4e72ba3c9a2 100644
--- a/addons/sale_timesheet/tests/test_timesheet_revenue.py
+++ b/addons/sale_timesheet/tests/test_timesheet_revenue.py
@@ -33,7 +33,8 @@ class TestSaleTimesheet(TestSale):
'uom_id': self.env.ref('product.product_uom_hour').id,
'uom_po_id': self.env.ref('product.product_uom_hour').id,
'default_code': 'SERV-DELI',
- 'track_service': 'task',
+ 'service_type': 'timesheet',
+ 'service_tracking': 'task_global_project',
'project_id': self.project.id
})
@@ -46,7 +47,8 @@ class TestSaleTimesheet(TestSale):
'uom_id': self.env.ref('product.product_uom_hour').id,
'uom_po_id': self.env.ref('product.product_uom_hour').id,
'default_code': 'SERV-ORDER',
- 'track_service': 'task',
+ 'service_type': 'timesheet',
+ 'service_tracking': 'task_global_project',
'project_id': self.project.id
})
diff --git a/addons/sale_timesheet/views/product_views.xml b/addons/sale_timesheet/views/product_views.xml
index 0c9903233a3..15704930b60 100644
--- a/addons/sale_timesheet/views/product_views.xml
+++ b/addons/sale_timesheet/views/product_views.xml
@@ -5,14 +5,17 @@
product.template
-
+
False
- {'invisible': [('type','!=','service')]}
+ {'invisible': [('type','==','service')]}
-
-
+
+
+
+
+
@@ -24,8 +27,9 @@
-
-
+
+
+
@@ -35,7 +39,7 @@
product.template
form
tree,form
- {'search_default_product_time_based':1, 'default_type': 'service', 'default_invoice_policy': 'delivery'}
+ {'search_default_product_time_based':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'timesheet'}
@@ -43,7 +47,15 @@
product.template
form
tree,form
- {'search_default_product_service_fixed':1, 'default_type': 'service', 'default_invoice_policy': 'order'}
+ {'search_default_product_service_fixed':1, 'default_type': 'service', 'default_invoice_policy': 'order', 'default_service_type': 'timesheet'}
+
+
+
+ Products
+ product.template
+ form
+ tree,form
+ {'search_default_product_service_milestone':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'manual'}
diff --git a/addons/sale_timesheet/views/res_config_settings_views.xml b/addons/sale_timesheet/views/res_config_settings_views.xml
index 7315c9ce029..bcc5b925374 100644
--- a/addons/sale_timesheet/views/res_config_settings_views.xml
+++ b/addons/sale_timesheet/views/res_config_settings_views.xml
@@ -15,6 +15,9 @@
+
+
+
diff --git a/odoo/addons/test_main_flows/static/src/js/tour.js b/odoo/addons/test_main_flows/static/src/js/tour.js
index 749cb958dfb..d3a10dabda8 100644
--- a/odoo/addons/test_main_flows/static/src/js/tour.js
+++ b/odoo/addons/test_main_flows/static/src/js/tour.js
@@ -212,11 +212,11 @@ tour.register('main_flow_tour', {
content: _t('Go to invoicing tab'),
position: 'bottom',
}, {
- trigger: '.o_field_widget[name=invoice_policy] .o_radio_input[data-value="delivery"]',
- content: _t('Change invoicing policy'),
+ trigger: '.o_field_widget[name=service_policy] .o_radio_input[data-value="delivered_timesheet"]',
+ content: _t('Change service policy'),
position: 'left',
}, {
- trigger: '.o_field_widget[name=track_service] input[data-value="task"]',
+ trigger: '.o_field_widget[name=service_tracking] input[data-value="task_global_project"]',
content: _t('Change track service'),
position: 'left',
}, {