diff --git a/addons/sale_project/views/product_views.xml b/addons/sale_project/views/product_views.xml index 47a0d2f1250..5afcd0a3b1e 100644 --- a/addons/sale_project/views/product_views.xml +++ b/addons/sale_project/views/product_views.xml @@ -8,8 +8,8 @@ - - + + diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 5003f72708c..ebf0da9f0e3 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -23,8 +23,8 @@ class AccountAnalyticLine(models.Model): so_line = fields.Many2one(compute="_compute_so_line", store=True, readonly=False) is_so_line_edited = fields.Boolean("Is Sales Order Item Manually Edited") - # TODO: [XBO] Since the task_id is not required in this model, then it should more efficient to depends to bill_type and pricing_type of project (See in master) - @api.depends('so_line.product_id', 'project_id', 'task_id', 'task_id.bill_type', 'task_id.pricing_type') + # TODO: [XBO] Since the task_id is not required in this model, then it should more efficient to depends to pricing_type of project (See in master) + @api.depends('so_line.product_id', 'project_id', 'task_id', 'task_id.pricing_type') def _compute_timesheet_invoice_type(self): for timesheet in self: if timesheet.project_id: # AAL will be set to False @@ -81,18 +81,16 @@ class AccountAnalyticLine(models.Model): on the one on the project """ if not task: - if project.bill_type == 'customer_project' and project.pricing_type == 'employee_rate': + if project.pricing_type == 'employee_rate': map_entry = self.env['project.sale.line.employee.map'].search([('project_id', '=', project.id), ('employee_id', '=', employee.id)]) if map_entry: return map_entry.sale_line_id if project.sale_line_id: return project.sale_line_id if task.allow_billable: - if task.bill_type == 'customer_task': + if task.pricing_type in ('task_rate', 'fixed_rate'): return task.sale_line_id - if task.pricing_type == 'fixed_rate': - return task.sale_line_id - elif task.pricing_type == 'employee_rate': + else: # then pricing_type = 'employee_rate' map_entry = project.sale_line_employee_ids.filtered(lambda map_entry: map_entry.employee_id == employee) if map_entry: return map_entry.sale_line_id diff --git a/addons/sale_timesheet/models/product.py b/addons/sale_timesheet/models/product.py index 143551cab6c..ce16030a947 100644 --- a/addons/sale_timesheet/models/product.py +++ b/addons/sale_timesheet/models/product.py @@ -19,8 +19,8 @@ class ProductTemplate(models.Model): ('timesheet', 'Timesheets on project (one fare per SO/Project)'), ], ondelete={'timesheet': 'set default'}) # override domain - project_id = fields.Many2one(domain="[('allow_billable', '=', True), ('bill_type', '=', 'customer_task'), ('allow_timesheets', 'in', [service_policy == 'delivered_timesheet' or '', True])]") - project_template_id = fields.Many2one(domain="[('allow_billable', '=', True), ('bill_type', '=', 'customer_project'), ('allow_timesheets', 'in', [service_policy == 'delivered_timesheet' or '', True])]") + project_id = fields.Many2one(domain="[('allow_billable', '=', True), ('pricing_type', '=', 'task_rate'), ('allow_timesheets', 'in', [service_policy == 'delivered_timesheet' or '', True])]") + project_template_id = fields.Many2one(domain="[('allow_billable', '=', True), ('pricing_type', 'in', ('fixed_rate', 'employee_rate')), ('allow_timesheets', 'in', [service_policy == 'delivered_timesheet' or '', True])]") def _default_visible_expense_policy(self): visibility = self.user_has_groups('project.group_project_user') diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index 61d8499ea0a..a6be8bd127c 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -24,16 +24,12 @@ class Project(models.Model): def _default_timesheet_product_id(self): return self.env.ref('sale_timesheet.time_product', False) - bill_type = fields.Selection([ - ('customer_task', 'Different customers'), - ('customer_project', 'A unique customer') - ], string="Invoice Tasks to", default="customer_task", - help='When billing tasks individually, a Sales Order will be created from each task. It is perfect if you would like to bill different services to different customers at different rates. \n When billing the whole project, a Sales Order will be created from the project instead. This option is better if you would like to bill all the tasks of a given project to a specific customer either at a fixed rate, or at an employee rate.') pricing_type = fields.Selection([ + ('task_rate', 'Task rate'), ('fixed_rate', 'Project rate'), ('employee_rate', 'Employee rate') - ], string="Pricing", default="fixed_rate", - help='The fixed rate is perfect if you bill a service at a fixed rate per hour or day worked regardless of the employee who performed it. The employee rate is preferable if your employees deliver the same service at a different rate. For instance, junior and senior consultants would deliver the same service (= consultancy), but at a different rate because of their level of seniority.') + ], string="Pricing", default="task_rate", + help='The task rate is perfect if you would like to bill different services to different customers at different rates. The fixed rate is perfect if you bill a service at a fixed rate per hour or day worked regardless of the employee who performed it. The employee rate is preferable if your employees deliver the same service at a different rate. For instance, junior and senior consultants would deliver the same service (= consultancy), but at a different rate because of their level of seniority.') sale_line_employee_ids = fields.One2many('project.sale.line.employee.map', 'project_id', "Sale line/Employee map", copy=False, help="Employee/Sale Order Item Mapping:\n Defines to which sales order item an employee's timesheet entry will be linked." "By extension, it defines the rate at which an employee's time on the project is billed.") @@ -63,11 +59,11 @@ class Project(models.Model): ] - @api.depends('allow_billable', 'sale_order_id', 'partner_id', 'bill_type') + @api.depends('allow_billable', 'sale_order_id', 'partner_id', 'pricing_type') def _compute_display_create_order(self): for project in self: show = True - if not project.partner_id or project.bill_type != 'customer_project' or not project.allow_billable or project.sale_order_id: + if not project.partner_id or project.pricing_type == 'task_rate' or not project.allow_billable or project.sale_order_id: show = False project.display_create_order = show @@ -80,9 +76,9 @@ class Project(models.Model): elif not project.timesheet_product_id: project.timesheet_product_id = default_product - @api.depends('pricing_type', 'allow_timesheets', 'allow_billable', 'sale_line_employee_ids', 'sale_line_employee_ids.employee_id', 'bill_type') + @api.depends('pricing_type', 'allow_timesheets', 'allow_billable', 'sale_line_employee_ids', 'sale_line_employee_ids.employee_id') def _compute_warning_employee_rate(self): - projects = self.filtered(lambda p: p.allow_billable and p.allow_timesheets and p.bill_type == 'customer_project' and p.pricing_type == 'employee_rate') + projects = self.filtered(lambda p: p.allow_billable and p.allow_timesheets and p.pricing_type == 'employee_rate') employees = self.env['account.analytic.line'].read_group([('task_id', 'in', projects.task_ids.ids)], ['employee_id', 'project_id'], ['employee_id', 'project_id'], ['employee_id', 'project_id'], lazy=False) dict_project_employee = defaultdict(list) for line in employees: @@ -208,7 +204,7 @@ class ProjectTask(models.Model): if not result.get('timesheet_product_id', False) and 'project_id' in result: project = self.env['project.project'].browse(result['project_id']) - if project.bill_type != 'customer_project' or project.pricing_type != 'employee_rate': + if project.pricing_type != 'employee_rate': result['timesheet_product_id'] = project.timesheet_product_id.id return result @@ -216,7 +212,6 @@ class ProjectTask(models.Model): sale_order_id = fields.Many2one(compute='_compute_sale_order_id', store=True, readonly=False, domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]") analytic_account_id = fields.Many2one('account.analytic.account', related='sale_order_id.analytic_account_id') - bill_type = fields.Selection(related="project_id.bill_type") pricing_type = fields.Selection(related="project_id.pricing_type") is_project_map_empty = fields.Boolean("Is Project map empty", compute='_compute_is_project_map_empty') has_multi_sol = fields.Boolean(compute='_compute_has_multi_sol', compute_sudo=True) @@ -290,7 +285,7 @@ class ProjectTask(models.Model): @api.onchange('project_id') def _onchange_project(self): - if self.project_id and self.project_id.bill_type == 'customer_project': + if self.project_id and self.project_id.pricing_type != 'task_rate': if not self.partner_id: self.partner_id = self.project_id.partner_id if not self.sale_line_id: @@ -301,7 +296,7 @@ class ProjectTask(models.Model): # Done after super to avoid constraints on field recomputation if values.get('project_id'): project_dest = self.env['project.project'].browse(values['project_id']) - if project_dest.bill_type == 'customer_project' and project_dest.pricing_type == 'employee_rate': + if project_dest.pricing_type == 'employee_rate': self.write({'sale_line_id': False}) return res @@ -311,7 +306,7 @@ class ProjectTask(models.Model): if not self.commercial_partner_id or not self.allow_billable: return False domain = [('is_service', '=', True), ('order_partner_id', 'child_of', self.commercial_partner_id.id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('remaining_hours', '>', 0)] - if self.project_id.bill_type == 'customer_project' and self.project_sale_order_id: + if self.project_id.pricing_type != 'task_rate' and self.project_sale_order_id: domain.append(('order_id', '=?', self.project_sale_order_id.id)) return self.env['sale.order.line'].search(domain, limit=1) diff --git a/addons/sale_timesheet/models/project_overview.py b/addons/sale_timesheet/models/project_overview.py index ad358d6ab00..2ffed770810 100644 --- a/addons/sale_timesheet/models/project_overview.py +++ b/addons/sale_timesheet/models/project_overview.py @@ -394,7 +394,7 @@ class Project(models.Model): task_order_line_ids = [ol['sale_line_id'][0] for ol in task_order_line_ids] if self.env.user.has_group('sales_team.group_sale_salesman'): - if self.bill_type == 'customer_project' and self.allow_billable and not self.sale_order_id: + if self.pricing_type != 'task_rate' and self.allow_billable and not self.sale_order_id: actions.append({ 'label': _("Create a Sales Order"), 'type': 'action', diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 3d9e92407aa..99f24efcbc2 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -13,7 +13,7 @@ class SaleOrder(models.Model): timesheet_count = fields.Float(string='Timesheet activities', compute='_compute_timesheet_ids', groups="hr_timesheet.group_hr_timesheet_user") # override domain - project_id = fields.Many2one(domain="['|', ('bill_type', '=', 'customer_task'), ('pricing_type', '=', 'fixed_rate'), ('analytic_account_id', '!=', False), ('company_id', '=', company_id)]") + project_id = fields.Many2one(domain="[('pricing_type', 'in', ('fixed_rate', 'task_rate')), ('analytic_account_id', '!=', False), ('company_id', '=', company_id)]") timesheet_encode_uom_id = fields.Many2one('uom.uom', related='company_id.timesheet_encode_uom_id') timesheet_total_duration = fields.Integer("Timesheet Total Duration", compute='_compute_timesheet_total_duration', help="Total recorded duration, expressed in the encoding UoM, and rounded to the unit") @@ -147,7 +147,7 @@ class SaleOrderLine(models.Model): if not line.is_expense and line.product_id.type == 'service' and line.product_id.service_type == 'timesheet': line.qty_delivered_method = 'timesheet' - @api.depends('analytic_line_ids.project_id', 'project_id.pricing_type', 'project_id.bill_type') + @api.depends('analytic_line_ids.project_id', 'project_id.pricing_type') def _compute_qty_delivered(self): super(SaleOrderLine, self)._compute_qty_delivered() @@ -182,7 +182,6 @@ class SaleOrderLine(models.Model): """Generate project values""" values = super()._timesheet_create_project_prepare_values() values['allow_billable'] = True - values['bill_type'] = 'customer_project' values['pricing_type'] = 'fixed_rate' return values diff --git a/addons/sale_timesheet/tests/test_project_billing.py b/addons/sale_timesheet/tests/test_project_billing.py index 50ab8abbdac..d7689b76dc9 100644 --- a/addons/sale_timesheet/tests/test_project_billing.py +++ b/addons/sale_timesheet/tests/test_project_billing.py @@ -11,7 +11,7 @@ class TestProjectBilling(TestCommonSaleTimesheet): @classmethod def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) - + # set up cls.employee_tde = cls.env['hr.employee'].create({ 'name': 'Employee TDE', @@ -97,7 +97,6 @@ class TestProjectBilling(TestCommonSaleTimesheet): 'name': "Project billed at Employee Rate", 'allow_timesheets': True, 'allow_billable': True, - 'bill_type': 'customer_project', 'pricing_type': 'employee_rate', 'sale_order_id': cls.sale_order_1.id, 'partner_id': cls.sale_order_1.partner_id.id, @@ -146,7 +145,6 @@ class TestProjectBilling(TestCommonSaleTimesheet): # Change project to billable at task rate self.project_non_billable.write({ 'allow_billable': True, - 'bill_type': 'customer_project', 'pricing_type': 'fixed_rate', }) task.timesheet_product_id = self.product_delivery_timesheet3 @@ -202,7 +200,6 @@ class TestProjectBilling(TestCommonSaleTimesheet): # Change project to billable at employee rate self.project_non_billable.write({ 'allow_billable': True, - 'bill_type': 'customer_project', 'pricing_type': 'employee_rate', }) @@ -260,7 +257,6 @@ class TestProjectBilling(TestCommonSaleTimesheet): }) self.assertTrue(task.allow_billable, "Task in project 'employee rate' should be billable") - self.assertEqual(task.bill_type, 'customer_project', "Task in project 'employee rate' should be billed at employee rate") self.assertEqual(task.pricing_type, 'employee_rate', "Task in project 'employee rate' should be billed at employee rate") self.assertFalse(task.sale_line_id, "Task created in a project billed on 'employee rate' should not be linked to a SOL") self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from the project") diff --git a/addons/sale_timesheet/tests/test_project_billing_multicompany.py b/addons/sale_timesheet/tests/test_project_billing_multicompany.py index ef4b855751c..5722688dd83 100644 --- a/addons/sale_timesheet/tests/test_project_billing_multicompany.py +++ b/addons/sale_timesheet/tests/test_project_billing_multicompany.py @@ -17,7 +17,7 @@ class TestProjectBillingMulticompany(TestCommonSaleTimesheet): 'name': "Non Billable Project", 'allow_timesheets': True, 'allow_billable': True, - 'bill_type': 'customer_project', + 'pricing_type': 'fixed_rate', 'company_id': cls.env.company.id, }) diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index 1efa5be560d..6957d3e64ee 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -10,7 +10,7 @@