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 + + + ft + + 3.28084 + smaller + + + mi + + + bigger + + + + + fl oz (US) + + 33.814 + smaller + + + qt (US) + + 1.05669 + smaller + + + gal (US) + + + bigger + + + in³ + + 61.0237 + smaller + + + ft³ + + 28.3168 + bigger + - - - lb - - 2.20462 - smaller - - - oz - - 35.274 - smaller - - - in - - 39.3701 - smaller - - - ft - - 3.28084 - smaller - - - mi - - - bigger - - - fl oz (US) - - 33.814 - smaller - - - qt (US) - - 1.05669 - smaller - - - gal (US) - - - bigger - - - in³ - - 61.0237 - smaller - - - ft³ - - 28.3168 - bigger - - 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')))