diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index ac7362b3f07..01aab06c87a 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -2898,7 +2898,7 @@ class AccountMoveLine(models.Model):
help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.")
currency_id = fields.Many2one('res.currency', string='Currency', required=True)
partner_id = fields.Many2one('res.partner', string='Partner', ondelete='restrict')
- product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
+ product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]", ondelete="restrict")
product_id = fields.Many2one('product.product', string='Product')
product_uom_category_id = fields.Many2one('uom.category', related='product_id.uom_id.category_id')
diff --git a/addons/hr_timesheet/models/uom.py b/addons/hr_timesheet/models/uom.py
index 885fc9de387..3a92715e99a 100644
--- a/addons/hr_timesheet/models/uom.py
+++ b/addons/hr_timesheet/models/uom.py
@@ -7,4 +7,12 @@ from odoo import fields, models
class Uom(models.Model):
_inherit = 'uom.uom'
+ def _unprotected_uom_xml_ids(self):
+ # Override
+ # When timesheet App is installed, we also need to protect the hour UoM
+ # from deletion (and warn in case of modification)
+ return [
+ "product_uom_dozen",
+ ]
+
timesheet_widget = fields.Char("Widget", help="Widget used in the webclient when this unit is the one used to encode timesheets.")
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 398730d6400..914ad11148c 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1361,7 +1361,7 @@ class SaleOrderLine(models.Model):
related="product_id.product_tmpl_id", domain=[('sale_ok', '=', True)])
product_updatable = fields.Boolean(compute='_compute_product_updatable', string='Can Edit Product', readonly=True, default=True)
product_uom_qty = fields.Float(string='Quantity', digits='Product Unit of Measure', required=True, default=1.0)
- product_uom = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
+ product_uom = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]", ondelete="restrict")
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id', readonly=True)
product_uom_readonly = fields.Boolean(compute='_compute_product_uom_readonly')
product_custom_attribute_value_ids = fields.One2many('product.attribute.custom.value', 'sale_order_line_id', string="Custom Values", copy=True)
diff --git a/addons/uom/__manifest__.py b/addons/uom/__manifest__.py
index d3eaaa9ed51..c8443071d1d 100644
--- a/addons/uom/__manifest__.py
+++ b/addons/uom/__manifest__.py
@@ -16,6 +16,9 @@ This is the base module for managing Units of measure.
'security/ir.model.access.csv',
'views/uom_uom_views.xml',
],
+ 'demo': [
+ 'data/uom_demo.xml',
+ ],
'installable': True,
'auto_install': False,
}
diff --git a/addons/uom/data/uom_data.xml b/addons/uom/data/uom_data.xml
index 3682dd17557..a41b5054561 100644
--- a/addons/uom/data/uom_data.xml
+++ b/addons/uom/data/uom_data.xml
@@ -1,164 +1,167 @@
-
-
-
-
- Unit
-
-
- Weight
-
-
- Working Time
-
-
- Length / Distance
-
-
- Volume
-
-
-
-
- Units
-
- reference
-
-
-
- Dozens
-
- bigger
-
+
-
-
- kg
-
- reference
-
-
-
- g
-
- smaller
-
+
+
+ Unit
+
+
+ Weight
+
+
+ Working Time
+
+
+ Length / Distance
+
+
+ Volume
+
-
- Days
-
-
- reference
-
-
- Hours
-
-
- smaller
-
-
-
-
- t
-
- bigger
-
-
-
- m
-
- reference
-
-
-
- km
-
- bigger
-
-
-
- cm
-
- smaller
-
-
- L
-
- 1.0
- reference
-
-
- m³
-
- 1000
- bigger
-
+
+
+
+
+ Units
+
+ reference
+
+
+
+
+ Days
+
+
+ reference
+
+
+ Hours
+
+
+ smaller
+
+
+
+
+
+ m
+
+ reference
+
+
+
+ km
+
+ bigger
+
+
+
+ cm
+
+ smaller
+
+
+
+
+ L
+
+ 1.0
+ reference
+
+
+ m³
+
+ 1000
+ bigger
+
+
+
+
+
+ kg
+
+ reference
+
+
+
+ g
+
+ smaller
+
+
+
+
+ t
+
+ bigger
+
+
+
+
+
+ lb
+
+ 2.20462
+ smaller
+
+
+ oz
+
+ 35.274
+ smaller
+
+
+
+
+ in
+
+ 39.3701
+ smaller
+
+
+
+ mi
+
+
+ bigger
+
+
+
+
+ fl oz (US)
+
+ 33.814
+ smaller
+
+
+ qt (US)
+
+ 1.05669
+ smaller
+
+
+ gal (US)
+
+
+ bigger
+
+
+ in³
+
+ 61.0237
+ smaller
+
+
-
-
- lb
-
- 2.20462
- smaller
-
-
- oz
-
- 35.274
- smaller
-
-
- in
-
- 39.3701
- smaller
-
-
-
- mi
-
-
- bigger
-
-
- fl oz (US)
-
- 33.814
- smaller
-
-
- qt (US)
-
- 1.05669
- smaller
-
-
- gal (US)
-
-
- bigger
-
-
- in³
-
- 61.0237
- smaller
-
-
-
diff --git a/addons/uom/data/uom_demo.xml b/addons/uom/data/uom_demo.xml
new file mode 100644
index 00000000000..74fe30b2876
--- /dev/null
+++ b/addons/uom/data/uom_demo.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+ Dozens
+
+ bigger
+
+
+
diff --git a/addons/uom/models/uom_uom.py b/addons/uom/models/uom_uom.py
index b8fa669e85c..216cc678716 100644
--- a/addons/uom/models/uom_uom.py
+++ b/addons/uom/models/uom_uom.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import timedelta
+
from odoo import api, fields, tools, models, _
from odoo.exceptions import UserError, ValidationError
@@ -11,22 +13,21 @@ class UoMCategory(models.Model):
name = fields.Char('Unit of Measure Category', required=True, translate=True)
- def unlink(self):
- uom_categ_unit = self.env.ref('uom.product_uom_categ_unit')
- uom_categ_wtime = self.env.ref('uom.uom_categ_wtime')
- if any(categ.id in (uom_categ_unit + uom_categ_wtime).ids for categ in self):
- raise UserError(_("You cannot delete this UoM Category as it is used by the system."))
- return super(UoMCategory, self).unlink()
-
class UoM(models.Model):
_name = 'uom.uom'
_description = 'Product Unit of Measure'
_order = "name"
+ def _unprotected_uom_xml_ids(self):
+ return [
+ "product_uom_hour", # NOTE: this uom is protected when hr_timesheet is installed.
+ "product_uom_dozen",
+ ]
+
name = fields.Char('Unit of Measure', required=True, translate=True)
category_id = fields.Many2one(
- 'uom.category', 'Category', required=True, ondelete='cascade',
+ 'uom.category', 'Category', required=True, ondelete='restrict',
help="Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios.")
factor = fields.Float(
'Ratio', default=1.0, digits=0, required=True, # force NUMERIC with unlimited precision
@@ -44,7 +45,7 @@ class UoM(models.Model):
('bigger', 'Bigger than the reference Unit of Measure'),
('reference', 'Reference Unit of Measure for this category'),
('smaller', 'Smaller than the reference Unit of Measure')], 'Type',
- default='reference', required=1)
+ default='reference', required=True)
_sql_constraints = [
('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!'),
@@ -62,6 +63,23 @@ class UoM(models.Model):
if self.uom_type == 'reference':
self.factor = 1
+ @api.onchange('factor', 'factor_inv', 'uom_type', 'rounding', 'category_id')
+ def _onchange_critical_fields(self):
+ if self._filter_protected_uoms() and self.create_date < (fields.Datetime.now() - timedelta(days=1)):
+ return {
+ 'warning': {
+ 'title': _("Warning for %s", self.name),
+ 'message': _(
+ "Some critical fields have been modified on %s.\n"
+ "Note that existing data WON'T be updated by this change.\n\n"
+ "As units of measure impact the whole system, this may cause critical issues.\n"
+ "E.g. modifying the rounding could disturb your inventory balance.\n\n"
+ "Therefore, changing core units of measure in a running database is not recommended.",
+ self.name,
+ )
+ }
+ }
+
@api.constrains('category_id', 'uom_type', 'active')
def _check_category_reference_uniqueness(self):
""" Force the existence of only one UoM reference per category
@@ -107,11 +125,13 @@ class UoM(models.Model):
return super(UoM, self).write(values)
def unlink(self):
- uom_categ_unit = self.env.ref('uom.product_uom_categ_unit')
- uom_categ_wtime = self.env.ref('uom.uom_categ_wtime')
- if any(uom.category_id.id in (uom_categ_unit + uom_categ_wtime).ids and uom.uom_type == 'reference' for uom in self):
- raise UserError(_("You cannot delete this UoM as it is used by the system. You should rather archive it."))
- return super(UoM, self).unlink()
+ locked_uoms = self._filter_protected_uoms()
+ if locked_uoms:
+ raise UserError(_(
+ "The following units of measure are used by the system and cannot be deleted: %s\nYou can archive them instead.",
+ ", ".join(locked_uoms.mapped('name')),
+ ))
+ return super().unlink()
@api.model
def name_create(self, name):
@@ -166,3 +186,16 @@ class UoM(models.Model):
if to_unit:
amount = amount / to_unit.factor
return amount
+
+ def _filter_protected_uoms(self):
+ """Verifies self does not contain protected uoms."""
+ linked_model_data = self.env['ir.model.data'].search([
+ ('model', '=', self._name),
+ ('res_id', 'in', self.ids),
+ ('module', '=', 'uom'),
+ ('name', 'not in', self._unprotected_uom_xml_ids()),
+ ])
+ if not linked_model_data:
+ return self.browse()
+ else:
+ return self.browse(set(linked_model_data.mapped('res_id')))