From ab76c421c279b26195396b61e66b3e8853785226 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Mon, 9 Nov 2020 11:04:39 +0000 Subject: [PATCH] [IMP] uom: protect/warn against uom changes/deletion 1) forbid deletion of main uoms & categories 2) warn modifications of main uoms data. Except for the working Hours uom, which isn't a strict UoM representation and could change between countries (and/or be deleted). 3) Also move the Dozen UoM to demo datas, it's a confusing UoM and isn't used by the majority of databases. 4) Restrict deletion of used uoms on main models. On sale orders and account moves the uoms couldn't be deleted thanks to the SQL constraint covering the display_type logic, but the generic error message is more clear for a user when deleting an uom. --- addons/account/models/account_move.py | 2 +- addons/hr_timesheet/models/uom.py | 8 + addons/sale/models/sale.py | 2 +- addons/uom/__manifest__.py | 3 + addons/uom/data/uom_data.xml | 321 +++++++++++++------------- addons/uom/data/uom_demo.xml | 11 + addons/uom/models/uom_uom.py | 61 +++-- 7 files changed, 233 insertions(+), 175 deletions(-) create mode 100644 addons/uom/data/uom_demo.xml 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')))