diff --git a/addons/sale_timesheet/__manifest__.py b/addons/sale_timesheet/__manifest__.py index 5afefc9b7ec..63c503ec8c7 100644 --- a/addons/sale_timesheet/__manifest__.py +++ b/addons/sale_timesheet/__manifest__.py @@ -15,6 +15,7 @@ have real delivered quantities in sales orders. """, 'depends': ['sale_project', 'hr_timesheet'], 'data': [ + 'data/sale_service_data.xml', 'security/ir.model.access.csv', 'security/sale_timesheet_security.xml', 'views/account_invoice_views.xml', diff --git a/addons/sale_timesheet/data/sale_service_data.xml b/addons/sale_timesheet/data/sale_service_data.xml new file mode 100644 index 00000000000..b0042b6acdc --- /dev/null +++ b/addons/sale_timesheet/data/sale_service_data.xml @@ -0,0 +1,12 @@ + + + + + Service on Timesheet + service + 40 + delivered_timesheet + + + + diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index a4a050d9649..48c1bf95259 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -5,9 +5,20 @@ from odoo import api, fields, models, _ from odoo.exceptions import ValidationError +# YTI PLEASE SPLIT ME class Project(models.Model): _inherit = 'project.project' + @api.model + def default_get(self, fields): + """ Pre-fill timesheet product as "Time" data product when creating new project allowing billable tasks by default. """ + result = super(Project, self).default_get(fields) + if 'timesheet_product_id' in fields and result.get('allow_billable') and result.get('allow_timesheets') and not result.get('timesheet_product_id'): + default_product = self.env.ref('sale_timesheet.time_product', False) + if default_product: + result['timesheet_product_id'] = default_product.id + return result + billable_type = fields.Selection([ ('task_rate', 'At Task Rate'), ('employee_rate', 'At Employee Rate'), @@ -22,6 +33,18 @@ class Project(models.Model): "By extension, it defines the rate at which an employee's time on the project is billed.") allow_billable = fields.Boolean("Bill from Tasks") display_create_order = fields.Boolean(compute='_compute_display_create_order') + timesheet_product_id = fields.Many2one( + 'product.product', string='Timesheet Product', + domain="""[ + ('type', '=', 'service'), + ('invoice_policy', '=', 'delivery'), + ('service_type', '=', 'timesheet'), + '|', ('company_id', '=', False), ('company_id', '=', company_id)]""", + help='Select a Service product with which you would like to bill your time spent on tasks.') + + _sql_constraints = [ + ('timesheet_product_required_if_billable_and_timesheets', "CHECK((allow_billable = 't' AND allow_timesheets = 't' AND timesheet_product_id IS NOT NULL) OR (allow_billable = 'f') OR (allow_timesheets = 'f'))", 'The timesheet product is required when the task can be billed and timesheets are allowed.'), + ] @api.depends('billable_type', 'allow_billable', 'sale_order_id', 'partner_id') def _compute_display_create_order(self): @@ -51,14 +74,24 @@ class Project(models.Model): if self.billable_type == 'no': self.sale_line_employee_ids = False - @api.onchange('allow_billable') - def _onchange_allow_billable(self): + @api.depends('allow_timesheets', 'allow_billable') + def _compute_timesheet_product_id(self): + default_product = self.env.ref('sale_timesheet.time_product', False) + for project in self: + if not project.allow_timesheets or not project.allow_billable: + project.timesheet_product_id = False + elif not project.timesheet_product_id: + project.timesheet_product_id = default_product + + @api.depends('allow_billable') + def _compute_allow_billable(self): """ In order to keep task billable type as 'task_rate' using sale_timesheet usual flow. (see _compute_billable_type method in sale_timesheet) """ - if self.allow_billable: - self.sale_order_id = False - self.sale_line_employee_ids = False + self.filtered(lambda p: p.allow_billable).update({ + 'sale_order_id': False, + 'sale_line_employee_ids': False, + }) @api.constrains('sale_line_id', 'billable_type') def _check_sale_line_type(self): @@ -118,6 +151,7 @@ class Project(models.Model): "context": { 'active_id': self.id, 'active_model': 'project.project', + 'default_product_id': self.timesheet_product_id.id, }, } @@ -233,6 +267,7 @@ class ProjectTask(models.Model): 'active_id': self.id, 'active_model': 'project.task', 'form_view_initial_mode': 'edit', + 'default_product_id': self.project_id.timesheet_product_id.id, }, } diff --git a/addons/sale_timesheet/static/img/product_product_time_product.png b/addons/sale_timesheet/static/img/product_product_time_product.png new file mode 100644 index 00000000000..c9d7a77d4f5 Binary files /dev/null and b/addons/sale_timesheet/static/img/product_product_time_product.png differ diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index b5c7aa3caba..064b16676b1 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -4,7 +4,7 @@ project.project.form.inherit project.project - + @@ -26,25 +26,18 @@ - - - - - project.view.form.inherit - project.project - - - + Invoice your time and material from tasks + @@ -52,6 +45,20 @@ + + project.project.view.form.simplified.inherit + project.project + + + + + + + + + + + project.project.kanban.inherit.sale.timesheet project.project diff --git a/addons/sale_timesheet/wizard/project_create_sale_order.py b/addons/sale_timesheet/wizard/project_create_sale_order.py index 7b6029e8296..aad2648a0d0 100644 --- a/addons/sale_timesheet/wizard/project_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_create_sale_order.py @@ -23,7 +23,8 @@ class ProjectCreateSalesOrder(models.TransientModel): if project.billable_type != 'no': raise UserError(_("The project is already billable.")) result['project_id'] = active_id - result['partner_id'] = project.partner_id.id + if not result.get('partner_id', False): + result['partner_id'] = project.partner_id.id return result project_id = fields.Many2one('project.project', "Project", domain=[('sale_line_id', '=', False)], help="Project for which we are creating a sales order", required=True) diff --git a/addons/sale_timesheet/wizard/project_task_create_sale_order.py b/addons/sale_timesheet/wizard/project_task_create_sale_order.py index e697f95592c..ffabb0ac9c7 100644 --- a/addons/sale_timesheet/wizard/project_task_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_task_create_sale_order.py @@ -23,7 +23,8 @@ class ProjectTaskCreateSalesOrder(models.TransientModel): if task.billable_type != 'no': raise UserError(_("The task is already billable.")) result['task_id'] = active_id - result['partner_id'] = task.partner_id.id + if not result.get('partner_id', False): + result['partner_id'] = task.partner_id.id return result task_id = fields.Many2one('project.task', "Task", domain=[('sale_line_id', '=', False)], help="Task for which we are creating a sales order", required=True)