From 40c99636c9318dcf8595dab79ae5812afae7b7b9 Mon Sep 17 00:00:00 2001 From: "Hugo Carlier (Huca)" Date: Mon, 3 Apr 2023 09:04:09 +0000 Subject: [PATCH] [IMP] sale_timesheet: initially set hours UoM for timesheet products - product form view: set 'hours' as the UoM if the service is either prepaid or invoice based on timesheets and if sale_timesheet is installed. - Details: Before this commit, the default UoM was the same for every invoice policy or product type selected. With this commit the UoM is automatically changed to 'Hours' when the product is of type service and the invoice policy is either prepaid (A) or based on timesheet (B). The value of the UoM can still be changed manually. The following corner cases are managed: - The user edits (or creates) a product, changes its type to service then to storable/consumable without saving it. The UoM should then be set to its initial value (before change) or its default value. - The user edits (or creates) a product, changes its invoice policy to A or B then again to another one without saving it. The UoM should then be set to its initial value (before change) or its default value. - The user edits a product of type service with invoice policy set to A (or B) and UoM that has manually been changed to something else than 'Hours'. He then modifies the invoice policy to B (or A) and then again to A (or B) without saving the changes. The UoM should then be set back to its initial value. Task-3072818 closes odoo/odoo#105951 Signed-off-by: Xavier Bol (xbo) --- addons/sale_timesheet/models/product.py | 28 +++++++- .../static/tests/tours/sale_timesheet_tour.js | 71 ++++++++++++++++++- .../tests/test_sale_timesheet_ui.py | 15 +--- 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/addons/sale_timesheet/models/product.py b/addons/sale_timesheet/models/product.py index 011944c0d85..9cdae7cb4f7 100644 --- a/addons/sale_timesheet/models/product.py +++ b/addons/sale_timesheet/models/product.py @@ -3,7 +3,7 @@ import threading -from odoo import api, fields, models, _ +from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError @@ -68,6 +68,17 @@ class ProductTemplate(models.Model): "project for the order to track the time spent." ) + @api.onchange('type', 'service_type', 'service_policy') + def _onchange_service_fields(self): + for record in self: + if record.type == 'service' and record.service_type == 'timesheet' and \ + not (record._origin.service_policy and record.service_policy == record._origin.service_policy): + record.uom_id = self.env.ref('uom.product_uom_hour') + elif record._origin.uom_id: + record.uom_id = record._origin.uom_id + else: + record.uom_id = self._get_default_uom_id() + def _get_service_to_general_map(self): return { **super()._get_service_to_general_map(), @@ -114,11 +125,26 @@ class ProductTemplate(models.Model): class ProductProduct(models.Model): _inherit = 'product.product' + @tools.ormcache() + def _get_default_uom_id(self): + return self.env.ref('uom.product_uom_unit') + def _is_delivered_timesheet(self): """ Check if the product is a delivered timesheet """ self.ensure_one() return self.type == 'service' and self.service_policy == 'delivered_timesheet' + @api.onchange('type', 'service_type', 'service_policy') + def _onchange_service_fields(self): + for record in self: + if record.type == 'service' and record.service_type == 'timesheet' and \ + not (record._origin.service_policy and record.service_policy == record._origin.service_policy): + record.uom_id = self.env.ref('uom.product_uom_hour') + elif record._origin.uom_id: + record.uom_id = record._origin.uom_id + else: + record.uom_id = self._get_default_uom_id() + @api.onchange('service_policy') def _onchange_service_policy(self): self.product_tmpl_id._inverse_service_policy() diff --git a/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js b/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js index f402185ad4c..0f05bdc1a6d 100644 --- a/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js +++ b/addons/sale_timesheet/static/tests/tours/sale_timesheet_tour.js @@ -9,6 +9,71 @@ registry.category("web_tour.tours").add('sale_timesheet_tour', { url: '/web', steps: [...stepUtils.goToAppSteps("sale.sale_menu_root", 'Go to the Sales App'), { + trigger: 'button[data-menu-xmlid="sale.product_menu_catalog"]', + content: 'Open Products menu', +}, { + trigger: 'a[data-menu-xmlid="sale.menu_product_template_action"]', + content: 'Go to product catalog', +}, { + trigger: 'button.o-kanban-button-new', + content: 'Create a new product', +}, { + trigger: 'textarea.o_input[id="name_0"]', + content: 'Enter a product name (e.g. Service Product Prepaid Hours)', + run: 'text Service Product Prepaid Hours', +}, { + trigger: 'input.o_input[id="uom_id_0"]', + content: 'Default unit should be Units', + run() { + const subjectValue = document.querySelector('input.o_input[id="uom_id_0"]').value; + if (subjectValue !== "Units") { + console.error( + `Default unit should be Units (actual: ${subjectValue})` + ); + } + }, +}, { + trigger: 'input.o_input[id="categ_id_0"]', + content: 'Enter a category name (e.g. Services)', + run: 'text Services', +}, { + trigger: 'a.dropdown-item:contains(Services)', + content: 'Select the ALL / Saleable / Services category', +}, { + trigger: 'select.o_input[id="detailed_type_0"]', + content: 'Select Service product type', + run: 'text "service"', +}, { + trigger: 'select.o_input[id="detailed_type_0"]', + content: 'Select Service product type', + run: 'click', +}, { + trigger: 'input.o_input[id="uom_id_0"]', + content: 'Unit should be set to Hours for a service product', + run() { + const subjectValue = document.querySelector('input.o_input[id="uom_id_0"]').value; + if (subjectValue !== "Hours") { + console.error( + `Default unit should be Hours (actual: ${subjectValue})` + ); + } + }, +}, { + trigger: 'input.o_input[id="list_price_0"]', + content: 'Choose a sales price for that product (e.g. 250$)', + run: 'text 250', +}, { + trigger: 'input.o_input[id="standard_price_0"]', + content: 'Set the cost of that product (e.g. 190$)', + run: 'text 190', +}, { + trigger: 'button[data-menu-xmlid="sale.sale_order_menu"]', + content: 'Open Order menu', +}, { + trigger: 'a[data-menu-xmlid="sale.menu_sale_quotations"]', + content: 'Go to quotations list', +}, { + trigger: 'button.o_list_button_add', content: 'Click on CREATE button to create a quotation with service products.', }, { @@ -24,10 +89,10 @@ registry.category("web_tour.tours").add('sale_timesheet_tour', { content: 'Click on "Add a product" to add a new product. We will add a service product.', }, { trigger: '.o_field_html[name="product_id"], .o_field_widget[name="product_template_id"] input', - content: Markup('Select a prepaid service product (e.g. Service Product (Prepaid Hours))'), - run: 'text Service Product (Prepaid Hours)', + content: 'Select a prepaid service product (e.g. Service Product Prepaid Hours)', + run: 'text Service Product Prepaid Hours', }, { - trigger: 'ul.ui-autocomplete a:contains(Service Product (Prepaid Hours))', + trigger: 'ul.ui-autocomplete a:contains(Service Product Prepaid Hours)', content: 'Select the prepaid service product in the autocomplete dropdown', }, { trigger: 'div[name="product_uom_qty"] input', diff --git a/addons/sale_timesheet/tests/test_sale_timesheet_ui.py b/addons/sale_timesheet/tests/test_sale_timesheet_ui.py index c1a0a11fae7..a9011f61cda 100644 --- a/addons/sale_timesheet/tests/test_sale_timesheet_ui.py +++ b/addons/sale_timesheet/tests/test_sale_timesheet_ui.py @@ -11,22 +11,9 @@ class TestUi(HttpCase): def setUpClass(cls): super().setUpClass() - service_category_id = cls.env['product.category'].create({ + cls.env['product.category'].create({ 'name': 'Services', 'parent_id': cls.env.ref('product.product_category_1').id, - }).id - - uom_hour_id = cls.env.ref('uom.product_uom_hour').id - cls.prepaid_service_product = cls.env['product.product'].create({ - 'name': 'Service Product (Prepaid Hours)', - 'categ_id': service_category_id, - 'type': 'service', - 'list_price': 250.00, - 'standard_price': 190.00, - 'uom_id': uom_hour_id, - 'uom_po_id': uom_hour_id, - 'service_policy': 'ordered_prepaid', - 'service_tracking': 'no', }) # Enable the "Milestones" feature to be able to create milestones on this tour.