[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) <xbo@odoo.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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 <i>(e.g. Service Product (Prepaid Hours))</i>'),
|
||||
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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user