[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.
This commit is contained in:
Victor Feyens
2020-11-27 16:10:01 +00:00
parent 87b5ddcf86
commit ab76c421c2
7 changed files with 233 additions and 175 deletions
+1 -1
View File
@@ -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')
+8
View File
@@ -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.")
+1 -1
View File
@@ -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)
+3
View File
@@ -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,
}
+162 -159
View File
@@ -1,164 +1,167 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!--
Resource: uom.category
-->
<record id="product_uom_categ_unit" model="uom.category">
<field name="name">Unit</field>
</record>
<record id="product_uom_categ_kgm" model="uom.category">
<field name="name">Weight</field>
</record>
<record id="uom_categ_wtime" model="uom.category">
<field name="name">Working Time</field>
</record>
<record id="uom_categ_length" model="uom.category">
<field name="name">Length / Distance</field>
</record>
<record id="product_uom_categ_vol" model="uom.category">
<field name="name">Volume</field>
</record>
<!--
Resource: uom.uom
-->
<record id="product_uom_unit" model="uom.uom">
<field name="category_id" ref="product_uom_categ_unit"/>
<field name="name">Units</field>
<field name="factor" eval="1.0"/>
<field name="uom_type">reference</field>
</record>
<record id="product_uom_dozen" model="uom.uom">
<field name="category_id" ref="uom.product_uom_categ_unit"/>
<field name="name">Dozens</field>
<field name="factor_inv" eval="12"/>
<field name="uom_type">bigger</field>
</record>
<odoo noupdate="1">
<record id="product_uom_kgm" model="uom.uom">
<field name="category_id" ref="product_uom_categ_kgm"/>
<field name="name">kg</field>
<field name="factor" eval="1"/>
<field name="uom_type">reference</field>
</record>
<record id="product_uom_gram" model="uom.uom">
<field name="category_id" ref="product_uom_categ_kgm"/>
<field name="name">g</field>
<field name="factor" eval="1000"/>
<field name="uom_type">smaller</field>
</record>
<!-- UOM Categories -->
<record id="product_uom_categ_unit" model="uom.category">
<field name="name">Unit</field>
</record>
<record id="product_uom_categ_kgm" model="uom.category">
<field name="name">Weight</field>
</record>
<record id="uom_categ_wtime" model="uom.category">
<field name="name">Working Time</field>
</record>
<record id="uom_categ_length" model="uom.category">
<field name="name">Length / Distance</field>
</record>
<record id="product_uom_categ_vol" model="uom.category">
<field name="name">Volume</field>
</record>
<record id="product_uom_day" model="uom.uom">
<field name="name">Days</field>
<field name="category_id" ref="uom_categ_wtime"/>
<field name="factor" eval="1.0"/>
<field name="uom_type">reference</field>
</record>
<record id="product_uom_hour" model="uom.uom">
<field name="name">Hours</field>
<field name="category_id" ref="uom_categ_wtime"/>
<field name="factor" eval="8.0"/>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_ton" model="uom.uom">
<field name="category_id" ref="product_uom_categ_kgm"/>
<!-- 'tonne' is the most common spelling in english-speaking countries,
the alternative is 'metric ton' in the US, abbreviated as 'mt' -->
<field name="name">t</field>
<field name="factor_inv" eval="1000"/>
<field name="uom_type">bigger</field>
</record>
<record id="product_uom_meter" model="uom.uom">
<field name="category_id" ref="uom_categ_length"/>
<field name="name">m</field>
<field name="factor" eval="1.0"/>
<field name="uom_type">reference</field>
</record>
<record id="product_uom_km" model="uom.uom">
<field name="category_id" ref="uom_categ_length"/>
<field name="name">km</field>
<field name="factor_inv" eval="1000"/>
<field name="uom_type">bigger</field>
</record>
<record id="product_uom_cm" model="uom.uom">
<field name="category_id" ref="uom_categ_length"/>
<field name="name">cm</field>
<field name="factor" eval="100"/>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_litre" model="uom.uom">
<field name="name">L</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor">1.0</field>
<field name="uom_type">reference</field>
</record>
<record id="product_uom_cubic_meter" model="uom.uom">
<field name="name">m³</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor_inv">1000</field>
<field name="uom_type">bigger</field>
</record>
<!-- UOM.UOM -->
<!-- Units -->
<record id="product_uom_unit" model="uom.uom">
<field name="category_id" ref="product_uom_categ_unit"/>
<field name="name">Units</field>
<field name="factor" eval="1.0"/>
<field name="uom_type">reference</field>
</record>
<!-- WORKING TIME -->
<record id="product_uom_day" model="uom.uom">
<field name="name">Days</field>
<field name="category_id" ref="uom_categ_wtime"/>
<field name="factor" eval="1.0"/>
<field name="uom_type">reference</field>
</record>
<record id="product_uom_hour" model="uom.uom">
<field name="name">Hours</field>
<field name="category_id" ref="uom_categ_wtime"/>
<field name="factor" eval="8.0"/>
<field name="uom_type">smaller</field>
</record>
<!-- LENGTH -->
<record id="product_uom_meter" model="uom.uom">
<field name="category_id" ref="uom_categ_length"/>
<field name="name">m</field>
<field name="factor" eval="1.0"/>
<field name="uom_type">reference</field>
</record>
<record id="product_uom_km" model="uom.uom">
<field name="category_id" ref="uom_categ_length"/>
<field name="name">km</field>
<field name="factor_inv" eval="1000"/>
<field name="uom_type">bigger</field>
</record>
<record id="product_uom_cm" model="uom.uom">
<field name="category_id" ref="uom_categ_length"/>
<field name="name">cm</field>
<field name="factor" eval="100"/>
<field name="uom_type">smaller</field>
</record>
<!-- VOLUME -->
<record id="product_uom_litre" model="uom.uom">
<field name="name">L</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor">1.0</field>
<field name="uom_type">reference</field>
</record>
<record id="product_uom_cubic_meter" model="uom.uom">
<field name="name">m³</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor_inv">1000</field>
<field name="uom_type">bigger</field>
</record>
<!-- WEIGHT -->
<record id="product_uom_kgm" model="uom.uom">
<field name="category_id" ref="product_uom_categ_kgm"/>
<field name="name">kg</field>
<field name="factor" eval="1"/>
<field name="uom_type">reference</field>
</record>
<record id="product_uom_gram" model="uom.uom">
<field name="category_id" ref="product_uom_categ_kgm"/>
<field name="name">g</field>
<field name="factor" eval="1000"/>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_ton" model="uom.uom">
<field name="category_id" ref="product_uom_categ_kgm"/>
<!-- 'tonne' is the most common spelling in english-speaking countries,
the alternative is 'metric ton' in the US, abbreviated as 'mt' -->
<field name="name">t</field>
<field name="factor_inv" eval="1000"/>
<field name="uom_type">bigger</field>
</record>
<!--Americanization of units of measure-->
<!-- WEIGHT -->
<record id="product_uom_lb" model="uom.uom">
<field name="name">lb</field>
<field name="category_id" ref="product_uom_categ_kgm"/>
<field name="factor">2.20462</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_oz" model="uom.uom">
<field name="name">oz</field>
<field name="category_id" ref="product_uom_categ_kgm"/>
<field name="factor">35.274</field>
<field name="uom_type">smaller</field>
</record>
<!-- LENGTH -->
<record id="product_uom_inch" model="uom.uom">
<field name="name">in</field>
<field name="category_id" ref="uom_categ_length"/>
<field name="factor">39.3701</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_foot" model="uom.uom">
<field name="name">ft</field>
<field name="category_id" ref="uom_categ_length"/>
<field name="factor">3.28084</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_mile" model="uom.uom">
<field name="name">mi</field>
<field name="category_id" ref="uom_categ_length"/>
<field name="factor_inv" eval="1609.34"/>
<field name="uom_type">bigger</field>
</record>
<!-- VOLUME -->
<record id="product_uom_floz" model="uom.uom">
<field name="name">fl oz (US)</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor">33.814</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_qt" model="uom.uom">
<field name="name">qt (US)</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor">1.05669</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_gal" model="uom.uom">
<field name="name">gal (US)</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor_inv" eval="3.78541"/>
<field name="uom_type">bigger</field>
</record>
<record id="product_uom_cubic_inch" model="uom.uom">
<field name="name">in³</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor">61.0237</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_cubic_foot" model="uom.uom">
<field name="name">ft³</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor_inv">28.3168</field>
<field name="uom_type">bigger</field>
</record>
<!--Americanization of units of measure-->
<record id="product_uom_lb" model="uom.uom">
<field name="name">lb</field>
<field name="category_id" ref="product_uom_categ_kgm"/>
<field name="factor">2.20462</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_oz" model="uom.uom">
<field name="name">oz</field>
<field name="category_id" ref="product_uom_categ_kgm"/>
<field name="factor">35.274</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_inch" model="uom.uom">
<field name="name">in</field>
<field name="category_id" ref="uom_categ_length"/>
<field name="factor">39.3701</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_foot" model="uom.uom">
<field name="name">ft</field>
<field name="category_id" ref="uom_categ_length"/>
<field name="factor">3.28084</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_mile" model="uom.uom">
<field name="name">mi</field>
<field name="category_id" ref="uom_categ_length"/>
<field name="factor_inv" eval="1609.34"/>
<field name="uom_type">bigger</field>
</record>
<record id="product_uom_floz" model="uom.uom">
<field name="name">fl oz (US)</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor">33.814</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_qt" model="uom.uom">
<field name="name">qt (US)</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor">1.05669</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_gal" model="uom.uom">
<field name="name">gal (US)</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor_inv" eval="3.78541"/>
<field name="uom_type">bigger</field>
</record>
<record id="product_uom_cubic_inch" model="uom.uom">
<field name="name">in³</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor">61.0237</field>
<field name="uom_type">smaller</field>
</record>
<record id="product_uom_cubic_foot" model="uom.uom">
<field name="name">ft³</field>
<field name="category_id" ref="product_uom_categ_vol"/>
<field name="factor_inv">28.3168</field>
<field name="uom_type">bigger</field>
</record>
</data>
</odoo>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo noupdate="1">
<record id="product_uom_dozen" model="uom.uom">
<field name="category_id" ref="uom.product_uom_categ_unit"/>
<field name="name">Dozens</field>
<field name="factor_inv" eval="12"/>
<field name="uom_type">bigger</field>
</record>
</odoo>
+47 -14
View File
@@ -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')))