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', }, {