|
- # -*- coding: utf-8 -*-
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
-
- from collections import defaultdict
- from odoo import api, fields, tools, models, _
- from odoo.exceptions import UserError, ValidationError
-
-
- class UoMCategory(models.Model):
- _name = 'uom.category'
- _description = 'Product UoM Categories'
-
- name = fields.Char('Unit of Measure Category', required=True, translate=True)
-
-
- class Uom(models.Model):
- _inherit = 'uom'
-
- category_id = fields.Many2one(
- 'uom.category', 'Category', required=True, ondelete='cascade',
- 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
- help='How much bigger or smaller this unit is compared to the reference Unit of Measure for this category: 1 * (reference unit) = ratio * (this unit)')
- factor_inv = fields.Float(
- 'Bigger Ratio', compute='_compute_factor_inv', digits=0, # force NUMERIC with unlimited precision
- readonly=True, required=True,
- help='How many times this Unit of Measure is bigger than the reference Unit of Measure in this category: 1 * (this unit) = ratio * (reference unit)')
- rounding = fields.Float(
- 'Rounding Precision', default=0.01, digits=0, required=True,
- help="The computed quantity will be a multiple of this value. "
- "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece.")
- uom_type = fields.Selection([
- ('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=True)
-
- _sql_constraints = [
- ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!'),
- ('rounding_gt_zero', 'CHECK (rounding>0)', 'The rounding precision must be strictly positive.'),
- ('factor_reference_is_one', "CHECK((uom_type = 'reference' AND factor = 1.0) OR (uom_type != 'reference'))", "The reference unit must have a conversion factor equal to 1.")
- ]
-
- @api.depends('factor')
- def _compute_factor_inv(self):
- for uom in self:
- uom.factor_inv = uom.factor and (1.0 / uom.factor) or 0.0
-
- @api.onchange('uom_type')
- def _onchange_uom_type(self):
- if self.uom_type == 'reference':
- self.factor = 1
-
- @api.constrains('category_id', 'uom_type', 'active')
- def _check_category_reference_uniqueness(self):
- categ_res = self.read_group(
- [("category_id", "in", self.category_id.ids)],
- ["category_id", "uom_type"],
- ["category_id", "uom_type"],
- lazy=False,
- )
- uom_by_category = defaultdict(int)
- ref_by_category = {}
- for res in categ_res:
- uom_by_category[res["category_id"][0]] += res["__count"]
- if res["uom_type"] == "reference":
- ref_by_category[res["category_id"][0]] = res["__count"]
-
- for category in self.category_id:
- reference_count = ref_by_category.get(category.id, 0)
- if reference_count > 1:
- raise ValidationError(_("UoM category %s should only have one reference unit of measure.", category.name))
- elif reference_count == 0 and uom_by_category.get(category.id, 0) > 0:
- raise ValidationError(_("UoM category %s should have a reference unit of measure.", category.name))
-
- @api.constrains('category_id')
- def _validate_uom_category(self):
- for uom in self:
- reference_uoms = self.env['uom'].search([
- ('category_id', '=', uom.category_id.id),
- ('uom_type', '=', 'reference')])
- if len(reference_uoms) > 1:
- raise ValidationError(_("UoM category %s should only have one reference unit of measure.") % (self.category_id.name))
-
- @api.model_create_multi
- def create(self, vals_list):
- for values in vals_list:
- if 'factor_inv' in values:
- factor_inv = values.pop('factor_inv')
- values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0
- return super(Uom, self).create(vals_list)
-
- def write(self, values):
- if 'factor_inv' in values:
- factor_inv = values.pop('factor_inv')
- values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0
- return super(Uom, self).write(values)
-
- def unlink(self):
- # UoM with external IDs shouldn't be deleted since they will most probably break the app somewhere else.
- # For example, in addons/product/models/product_template.py, cubic meters are used in `_get_volume_uom_id_from_ir_config_parameter()`,
- # meters in `_get_length_uom_id_from_ir_config_parameter()`, and so on.
- if self.env['ir.model.data'].search_count([('model', '=', self._name), ('res_id', 'in', self.ids)]):
- raise UserError(_("You cannot delete this UoM as it is used by the system. You should rather archive it."))
- return super(Uom, self).unlink()
-
- @api.model
- def name_create(self, name):
- """ The UoM category and factor are required, so we'll have to add temporary values
- for imported UoMs """
- values = {
- self._rec_name: name,
- 'factor': 1
- }
- # look for the category based on the english name, i.e. no context on purpose!
- # TODO: should find a way to have it translated but not created until actually used
- if not self._context.get('default_category_id'):
- EnglishUoMCateg = self.env['uom.category'].with_context({})
- misc_category = EnglishUoMCateg.search([('name', '=', 'Unsorted/Imported Units')])
- if misc_category:
- values['category_id'] = misc_category.id
- else:
- values['category_id'] = EnglishUoMCateg.name_create('Unsorted/Imported Units')[0]
- new_uom = self.create(values)
- return new_uom.name_get()[0]
-
- def _compute_quantity(self, qty, to_unit, round=True, rounding_method='UP', raise_if_failure=True):
- """ Convert the given quantity from the current UoM `self` into a given one
- :param qty: the quantity to convert
- :param to_unit: the destination UoM record (uom)
- :param raise_if_failure: only if the conversion is not possible
- - if true, raise an exception if the conversion is not possible (different UoM category),
- - otherwise, return the initial quantity
- """
- if not self or not qty:
- return qty
- self.ensure_one()
-
- if self != to_unit and self.category_id.id != to_unit.category_id.id:
- if raise_if_failure:
- raise UserError(_('The unit of measure %s defined on the order line doesn\'t belong to the same category as the unit of measure %s defined on the product. Please correct the unit of measure defined on the order line or on the product, they should belong to the same category.') % (self.name, to_unit.name))
- else:
- return qty
-
- if self == to_unit:
- amount = qty
- else:
- amount = qty / self.factor
- if to_unit:
- amount = amount * to_unit.factor
-
- if to_unit and round:
- amount = tools.float_round(amount, precision_rounding=to_unit.rounding, rounding_method=rounding_method)
-
- return amount
-
- def _compute_price(self, price, to_unit):
- self.ensure_one()
- if not self or not price or not to_unit or self == to_unit:
- return price
- if self.category_id.id != to_unit.category_id.id:
- return price
- amount = price * self.factor
- if to_unit:
- amount = amount / to_unit.factor
- return amount
|