mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 02:52:02 +02:00
230 lines
9.6 KiB
Python
230 lines
9.6 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
from typing import TYPE_CHECKING, Literal
|
|
|
|
from odoo import _, api, fields, models, tools
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import float_round
|
|
|
|
if TYPE_CHECKING:
|
|
from odoo.orm.types import Self
|
|
from odoo.tools.float_utils import RoundingMethod
|
|
|
|
|
|
class UomUom(models.Model):
|
|
_name = 'uom.uom'
|
|
_description = 'Product Unit of Measure'
|
|
_parent_name = 'relative_uom_id'
|
|
_parent_store = True
|
|
_order = 'sequence, relative_uom_id, id'
|
|
|
|
def _unprotected_uom_xml_ids(self):
|
|
""" Return a list of UoM XML IDs that are not protected by default.
|
|
Note: Some of these may be protected via overrides in other modules.
|
|
"""
|
|
return [
|
|
"product_uom_hour",
|
|
"product_uom_dozen",
|
|
"product_uom_pack_6",
|
|
]
|
|
|
|
name = fields.Char('Unit Name', required=True, translate=True)
|
|
sequence = fields.Integer(compute="_compute_sequence", store=True, readonly=False, precompute=True)
|
|
relative_factor = fields.Float(
|
|
'Contains', 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 UoM for this unit')
|
|
rounding = fields.Float('Rounding Precision', compute="_compute_rounding")
|
|
active = fields.Boolean('Active', default=True, help="Uncheck the active field to disable a unit of measure without deleting it.")
|
|
relative_uom_id = fields.Many2one('uom.uom', 'Reference Unit', ondelete='cascade', index='btree_not_null')
|
|
related_uom_ids = fields.One2many('uom.uom', 'relative_uom_id', 'Related UoMs')
|
|
factor = fields.Float('Absolute Quantity', digits=0, compute='_compute_factor', recursive=True, store=True)
|
|
parent_path = fields.Char(index=True)
|
|
|
|
_factor_gt_zero = models.Constraint(
|
|
'CHECK (relative_factor!=0)',
|
|
'The conversion ratio for a unit of measure cannot be 0!',
|
|
)
|
|
|
|
# === COMPUTE METHODS === #
|
|
|
|
@api.depends('relative_factor')
|
|
def _compute_sequence(self):
|
|
for uom in self:
|
|
if uom.id and uom.sequence:
|
|
# Only set a default sequence before the record creation, or on module update if
|
|
# there is no value.
|
|
continue
|
|
uom.sequence = min(int(uom.relative_factor * 100.0), 1000)
|
|
|
|
def _compute_rounding(self):
|
|
""" All Units of Measure share the same rounding precision defined in 'Product Unit'.
|
|
Set in a compute to ensure compatibility with previous calls to `uom.rounding`.
|
|
"""
|
|
decimal_precision = self.env['decimal.precision'].precision_get('Product Unit')
|
|
self.rounding = 10 ** -decimal_precision
|
|
|
|
@api.depends('relative_factor', 'relative_uom_id', 'relative_uom_id.factor')
|
|
def _compute_factor(self):
|
|
for uom in self:
|
|
if uom.relative_uom_id:
|
|
uom.factor = uom.relative_factor * uom.relative_uom_id.factor
|
|
else:
|
|
uom.factor = uom.relative_factor
|
|
|
|
# === ONCHANGE METHODS === #
|
|
|
|
@api.onchange('relative_factor')
|
|
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"
|
|
"Therefore, changing core units of measure in a running database is not recommended.",
|
|
self.name,
|
|
)
|
|
}
|
|
}
|
|
|
|
# === CONSTRAINT METHODS === #
|
|
|
|
@api.constrains('relative_factor', 'relative_uom_id')
|
|
def _check_factor(self):
|
|
for uom in self:
|
|
if not uom.relative_uom_id and uom.relative_factor != 1.0:
|
|
raise UserError(_("Reference unit of measure is missing."))
|
|
|
|
# === CRUD METHODS === #
|
|
|
|
@api.ondelete(at_uninstall=False)
|
|
def _unlink_except_master_data(self):
|
|
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')),
|
|
))
|
|
|
|
# === BUSINESS METHODS === #
|
|
|
|
def round(self, value: float, rounding_method: RoundingMethod = 'HALF-UP') -> float:
|
|
"""Round the value using the 'Product Unit' precision"""
|
|
self.ensure_one()
|
|
digits = self.env['decimal.precision'].precision_get('Product Unit')
|
|
return tools.float_round(value, precision_digits=digits, rounding_method=rounding_method)
|
|
|
|
def compare(self, value1: float, value2: float) -> Literal[-1, 0, 1]:
|
|
"""Compare two measures after rounding them with the 'Product Unit' precision
|
|
|
|
:param value1: origin value to compare
|
|
:param value2: value to compare to
|
|
:return: -1, 0 or 1, if ``value1`` is lower than, equal to, or greater than ``value2``.
|
|
"""
|
|
self.ensure_one()
|
|
digits = self.env['decimal.precision'].precision_get('Product Unit')
|
|
return tools.float_compare(value1, value2, precision_digits=digits)
|
|
|
|
def is_zero(self, value: float) -> bool:
|
|
"""Check if the value is zero after rounding with the 'Product Unit' precision"""
|
|
self.ensure_one()
|
|
digits = self.env['decimal.precision'].precision_get('Product Unit')
|
|
return tools.float_is_zero(value, precision_digits=digits)
|
|
|
|
@api.depends('name', 'relative_factor', 'relative_uom_id')
|
|
@api.depends_context('formatted_display_name')
|
|
def _compute_display_name(self):
|
|
super()._compute_display_name()
|
|
for uom in self:
|
|
if uom.env.context.get('formatted_display_name') and uom.relative_uom_id:
|
|
uom.display_name = f"{uom.name}\t--{uom.relative_factor} {uom.relative_uom_id.name}--"
|
|
|
|
def _compute_quantity(
|
|
self,
|
|
qty: float,
|
|
to_unit: Self,
|
|
round: bool = True,
|
|
rounding_method: RoundingMethod = 'UP',
|
|
raise_if_failure: bool = True,
|
|
) -> float:
|
|
""" Convert the given quantity from the current UoM `self` into a given one
|
|
:param qty: the quantity to convert
|
|
:param to_unit: the destination UomUom record (uom.uom)
|
|
:param raise_if_failure: only if the conversion is not possible
|
|
- if true, raise an exception if the conversion is not possible (different UomUom category),
|
|
- otherwise, return the initial quantity
|
|
"""
|
|
if not self or not qty:
|
|
return qty
|
|
self.ensure_one()
|
|
|
|
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 _check_qty(self, product_qty, uom_id, rounding_method="HALF-UP"):
|
|
"""Check if product_qty in given uom is a multiple of the packaging qty.
|
|
If not, rounding the product_qty to closest multiple of the packaging qty
|
|
according to the rounding_method "UP", "HALF-UP or "DOWN".
|
|
"""
|
|
self.ensure_one()
|
|
packaging_qty = self._compute_quantity(1, uom_id)
|
|
if self == uom_id:
|
|
return product_qty
|
|
# We do not use the modulo operator to check if qty is a mltiple of q. Indeed the quantity
|
|
# per package might be a float, leading to incorrect results. For example:
|
|
# 8 % 1.6 = 1.5999999999999996
|
|
# 5.4 % 1.8 = 2.220446049250313e-16
|
|
if product_qty and packaging_qty:
|
|
product_qty = float_round(product_qty / packaging_qty, precision_rounding=1.0,
|
|
rounding_method=rounding_method) * packaging_qty
|
|
return product_qty
|
|
|
|
def _compute_price(self, price: float, to_unit: Self) -> float:
|
|
self.ensure_one()
|
|
if not self or not price or not to_unit or self == to_unit:
|
|
return price
|
|
amount = price * to_unit.factor
|
|
if to_unit:
|
|
amount = amount / self.factor
|
|
return amount
|
|
|
|
def _filter_protected_uoms(self):
|
|
"""Verifies self does not contain protected uoms."""
|
|
linked_model_data = self.env['ir.model.data'].sudo().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')))
|
|
|
|
def _has_common_reference(self, other_uom: Self) -> bool:
|
|
""" Check if `self` and `other_uom` have a common reference unit """
|
|
self.ensure_one()
|
|
other_uom.ensure_one()
|
|
self_path = self.parent_path.split('/')
|
|
other_path = other_uom.parent_path.split('/')
|
|
common_path = []
|
|
for self_parent, other_parent in zip(self_path, other_path):
|
|
if self_parent == other_parent:
|
|
common_path.append(self_parent)
|
|
else:
|
|
break
|
|
return bool(common_path)
|