2014-12-02 12:55:30 +01:00
|
|
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
|
|
|
# this repository contains the full copyright notices and license terms.
|
2018-06-26 19:11:48 +02:00
|
|
|
|
2010-08-15 00:23:46 +01:00
|
|
|
from decimal import Decimal
|
2017-02-02 12:11:10 +01:00
|
|
|
from math import ceil, floor, log10
|
2013-09-06 22:34:24 +02:00
|
|
|
|
2019-07-08 17:34:44 +02:00
|
|
|
from trytond.config import config
|
2018-12-29 14:20:29 +01:00
|
|
|
from trytond.i18n import gettext
|
2018-03-26 14:10:59 +02:00
|
|
|
from trytond.model import ModelView, ModelSQL, DeactivableMixin, fields, Check
|
2018-12-29 14:20:29 +01:00
|
|
|
from trytond.model.exceptions import AccessError
|
2011-08-21 11:20:46 +02:00
|
|
|
from trytond.pyson import Eval
|
2010-08-15 00:23:46 +01:00
|
|
|
from trytond.transaction import Transaction
|
2008-03-03 18:36:08 +01:00
|
|
|
|
2018-12-29 14:20:29 +01:00
|
|
|
from .exceptions import UOMValidationError
|
|
|
|
|
|
2020-03-01 12:33:52 +01:00
|
|
|
__all__ = ['uom_conversion_digits']
|
2012-09-24 22:18:00 +02:00
|
|
|
|
2019-07-08 17:34:44 +02:00
|
|
|
uom_conversion_digits = (
|
|
|
|
|
config.getint('product', 'uom_conversion_decimal', default=12),) * 2
|
|
|
|
|
|
2012-04-03 20:07:18 +02:00
|
|
|
|
2009-02-28 17:00:24 +01:00
|
|
|
class UomCategory(ModelSQL, ModelView):
|
2008-03-03 18:36:08 +01:00
|
|
|
'Product uom category'
|
2012-09-24 22:18:00 +02:00
|
|
|
__name__ = 'product.uom.category'
|
2008-09-17 12:32:45 +02:00
|
|
|
name = fields.Char('Name', required=True, translate=True)
|
2008-04-08 17:25:39 +02:00
|
|
|
uoms = fields.One2Many('product.uom', 'category', 'Unit of Measures')
|
2008-03-03 18:36:08 +01:00
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@classmethod
|
|
|
|
|
def __setup__(cls):
|
|
|
|
|
super(UomCategory, cls).__setup__()
|
|
|
|
|
cls._order.insert(0, ('name', 'ASC'))
|
2008-03-03 18:36:08 +01:00
|
|
|
|
|
|
|
|
|
2018-03-26 14:10:59 +02:00
|
|
|
class Uom(DeactivableMixin, ModelSQL, ModelView):
|
2008-03-03 18:36:08 +01:00
|
|
|
'Unit of measure'
|
2012-09-24 22:18:00 +02:00
|
|
|
__name__ = 'product.uom'
|
2020-03-17 20:08:12 +01:00
|
|
|
name = fields.Char("Name", size=None, required=True, translate=True)
|
|
|
|
|
symbol = fields.Char("Symbol", size=10, required=True, translate=True)
|
|
|
|
|
category = fields.Many2One(
|
|
|
|
|
'product.uom.category', "Category", required=True, ondelete='RESTRICT')
|
2019-07-08 17:34:44 +02:00
|
|
|
rate = fields.Float(
|
|
|
|
|
"Rate", digits=uom_conversion_digits, required=True,
|
2013-02-24 14:18:51 +01:00
|
|
|
help=('The coefficient for the formula:\n'
|
|
|
|
|
'1 (base unit) = coef (this unit)'))
|
2019-07-08 17:34:44 +02:00
|
|
|
factor = fields.Float(
|
|
|
|
|
"Factor", digits=uom_conversion_digits, required=True,
|
2013-02-24 14:18:51 +01:00
|
|
|
help=('The coefficient for the formula:\n'
|
|
|
|
|
'coef (base unit) = 1 (this unit)'))
|
2020-03-17 20:08:12 +01:00
|
|
|
rounding = fields.Float(
|
|
|
|
|
"Rounding Precision", digits=(12, Eval('digits', 12)), required=True,
|
2015-02-24 18:19:58 +01:00
|
|
|
domain=[
|
|
|
|
|
('rounding', '>', 0),
|
2020-03-17 20:08:12 +01:00
|
|
|
],
|
|
|
|
|
depends=['digits'])
|
2012-03-23 18:38:50 +01:00
|
|
|
digits = fields.Integer('Display Digits', required=True)
|
2008-03-03 18:36:08 +01:00
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@classmethod
|
|
|
|
|
def __setup__(cls):
|
|
|
|
|
super(Uom, cls).__setup__()
|
2015-06-22 16:59:33 +02:00
|
|
|
t = cls.__table__()
|
2012-09-24 22:18:00 +02:00
|
|
|
cls._sql_constraints += [
|
2015-06-22 16:59:33 +02:00
|
|
|
('non_zero_rate_factor', Check(t, (t.rate != 0) | (t.factor != 0)),
|
2018-12-29 14:20:29 +01:00
|
|
|
'product.msg_uom_no_zero_factor_rate')
|
2012-09-24 22:18:00 +02:00
|
|
|
]
|
|
|
|
|
cls._order.insert(0, ('name', 'ASC'))
|
2009-04-06 13:03:51 +02:00
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@classmethod
|
|
|
|
|
def check_xml_record(cls, records, values):
|
2008-08-26 18:18:38 +02:00
|
|
|
return True
|
|
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@staticmethod
|
|
|
|
|
def default_rate():
|
2008-03-03 18:36:08 +01:00
|
|
|
return 1.0
|
|
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@staticmethod
|
|
|
|
|
def default_factor():
|
2008-03-03 18:36:08 +01:00
|
|
|
return 1.0
|
|
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@staticmethod
|
|
|
|
|
def default_rounding():
|
2008-03-03 18:36:08 +01:00
|
|
|
return 0.01
|
|
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@staticmethod
|
|
|
|
|
def default_digits():
|
2008-08-26 19:06:12 +02:00
|
|
|
return 2
|
|
|
|
|
|
2014-01-26 14:29:18 +01:00
|
|
|
@fields.depends('factor')
|
2012-09-24 22:18:00 +02:00
|
|
|
def on_change_factor(self):
|
|
|
|
|
if (self.factor or 0.0) == 0.0:
|
2014-10-29 20:39:55 +01:00
|
|
|
self.rate = 0.0
|
|
|
|
|
else:
|
2019-07-08 17:34:44 +02:00
|
|
|
self.rate = round(1.0 / self.factor, uom_conversion_digits[1])
|
2008-03-03 18:36:08 +01:00
|
|
|
|
2014-01-26 14:29:18 +01:00
|
|
|
@fields.depends('rate')
|
2012-09-24 22:18:00 +02:00
|
|
|
def on_change_rate(self):
|
|
|
|
|
if (self.rate or 0.0) == 0.0:
|
2014-10-29 20:39:55 +01:00
|
|
|
self.factor = 0.0
|
|
|
|
|
else:
|
2014-12-02 12:55:30 +01:00
|
|
|
self.factor = round(
|
2019-07-08 17:34:44 +02:00
|
|
|
1.0 / self.rate, uom_conversion_digits[1])
|
2008-03-13 01:22:57 +01:00
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@classmethod
|
|
|
|
|
def search_rec_name(cls, name, clause):
|
2015-04-05 23:36:13 +02:00
|
|
|
if clause[1].startswith('!') or clause[1].startswith('not '):
|
|
|
|
|
bool_op = 'AND'
|
|
|
|
|
else:
|
|
|
|
|
bool_op = 'OR'
|
|
|
|
|
return [bool_op,
|
2014-02-24 20:21:33 +01:00
|
|
|
(cls._rec_name,) + tuple(clause[1:]),
|
|
|
|
|
('symbol',) + tuple(clause[1:]),
|
|
|
|
|
]
|
2009-05-19 18:32:01 +02:00
|
|
|
|
2016-01-11 10:28:00 -03:00
|
|
|
def round(self, number):
|
2017-02-02 12:11:10 +01:00
|
|
|
return _round(self, number, func=round)
|
|
|
|
|
|
|
|
|
|
def ceil(self, number):
|
|
|
|
|
return _round(self, number, func=ceil)
|
|
|
|
|
|
|
|
|
|
def floor(self, number):
|
|
|
|
|
return _round(self, number, func=floor)
|
2008-03-07 00:36:18 +01:00
|
|
|
|
2013-03-07 16:55:11 +01:00
|
|
|
@classmethod
|
|
|
|
|
def validate(cls, uoms):
|
|
|
|
|
super(Uom, cls).validate(uoms)
|
|
|
|
|
for uom in uoms:
|
|
|
|
|
uom.check_factor_and_rate()
|
|
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
def check_factor_and_rate(self):
|
2010-03-11 15:29:21 +01:00
|
|
|
"Check coherence between factor and rate"
|
2012-09-24 22:18:00 +02:00
|
|
|
if self.rate == self.factor == 0.0:
|
2013-10-15 15:09:04 +02:00
|
|
|
return
|
2012-09-24 22:18:00 +02:00
|
|
|
if (self.rate != round(
|
2019-07-08 17:34:44 +02:00
|
|
|
1.0 / self.factor, uom_conversion_digits[1])
|
2012-09-24 22:18:00 +02:00
|
|
|
and self.factor != round(
|
2019-07-08 17:34:44 +02:00
|
|
|
1.0 / self.rate, uom_conversion_digits[1])):
|
2018-12-29 14:20:29 +01:00
|
|
|
raise UOMValidationError(
|
|
|
|
|
gettext('product.msg_uom_incompatible_factor_rate',
|
|
|
|
|
uom=self.rec_name))
|
2008-03-11 21:38:51 +01:00
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@classmethod
|
2013-12-19 14:45:07 +01:00
|
|
|
def write(cls, *args):
|
2010-08-15 00:23:46 +01:00
|
|
|
if Transaction().user == 0:
|
2013-12-19 14:45:07 +01:00
|
|
|
super(Uom, cls).write(*args)
|
2012-09-24 22:18:00 +02:00
|
|
|
return
|
2009-04-07 15:27:19 +02:00
|
|
|
|
2013-12-19 14:45:07 +01:00
|
|
|
actions = iter(args)
|
|
|
|
|
all_uoms = []
|
|
|
|
|
for uoms, values in zip(actions, actions):
|
|
|
|
|
if 'rate' not in values and 'factor' not in values \
|
|
|
|
|
and 'category' not in values:
|
|
|
|
|
continue
|
|
|
|
|
all_uoms += uoms
|
|
|
|
|
|
2018-12-29 14:20:29 +01:00
|
|
|
old_uom = dict((uom.id, (uom.factor, uom.rate, uom.category))
|
2013-12-19 14:45:07 +01:00
|
|
|
for uom in all_uoms)
|
2009-04-07 15:27:19 +02:00
|
|
|
|
2013-12-19 14:45:07 +01:00
|
|
|
super(Uom, cls).write(*args)
|
2009-04-07 15:27:19 +02:00
|
|
|
|
2013-12-19 14:45:07 +01:00
|
|
|
for uom in all_uoms:
|
2020-01-20 11:12:47 +01:00
|
|
|
for i, field in enumerate(['factor', 'rate', 'category']):
|
2018-12-29 14:20:29 +01:00
|
|
|
if getattr(uom, field) != old_uom[uom.id][i]:
|
|
|
|
|
raise AccessError(
|
|
|
|
|
gettext('product.msg_uom_modify_%s' % field,
|
|
|
|
|
uom=uom.rec_name),
|
|
|
|
|
gettext('product.msg_uom_modify_options'))
|
2008-03-07 00:36:18 +01:00
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@property
|
|
|
|
|
def accurate_field(self):
|
2008-03-07 00:36:18 +01:00
|
|
|
"""
|
2010-03-11 15:29:21 +01:00
|
|
|
Select the more accurate field.
|
|
|
|
|
It chooses the field that has the least decimal.
|
|
|
|
|
"""
|
2019-07-08 17:34:44 +02:00
|
|
|
return _accurate_operator(self.factor, self.rate)
|
2010-03-11 15:29:21 +01:00
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@classmethod
|
2019-07-08 17:34:44 +02:00
|
|
|
def compute_qty(cls, from_uom, qty, to_uom, round=True,
|
|
|
|
|
factor=None, rate=None):
|
2010-03-11 15:29:21 +01:00
|
|
|
"""
|
|
|
|
|
Convert quantity for given uom's.
|
2019-07-08 17:34:44 +02:00
|
|
|
|
|
|
|
|
When converting between uom's from different categories the factor and
|
|
|
|
|
rate provide the ratio to use to convert between the category's base
|
|
|
|
|
uom's.
|
2008-03-07 00:36:18 +01:00
|
|
|
"""
|
2015-07-29 17:48:28 +02:00
|
|
|
if not qty or (from_uom is None and to_uom is None):
|
2008-03-07 00:36:18 +01:00
|
|
|
return qty
|
2015-07-29 17:48:28 +02:00
|
|
|
if from_uom is None:
|
|
|
|
|
raise ValueError("missing from_uom")
|
|
|
|
|
if to_uom is None:
|
|
|
|
|
raise ValueError("missing to_uom")
|
2010-03-11 15:29:21 +01:00
|
|
|
if from_uom.category.id != to_uom.category.id:
|
2019-07-08 17:34:44 +02:00
|
|
|
if not factor and not rate:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"cannot convert between %s and %s without a factor or rate"
|
2015-07-29 17:48:28 +02:00
|
|
|
% (from_uom.category.name, to_uom.category.name))
|
2019-07-08 17:34:44 +02:00
|
|
|
elif factor or rate:
|
|
|
|
|
raise ValueError("factor and rate not allowed for same category")
|
2015-07-29 17:48:28 +02:00
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
if from_uom.accurate_field == 'factor':
|
2008-03-13 01:22:57 +01:00
|
|
|
amount = qty * from_uom.factor
|
2008-03-07 00:36:18 +01:00
|
|
|
else:
|
2008-03-13 01:22:57 +01:00
|
|
|
amount = qty / from_uom.rate
|
2015-07-29 17:48:28 +02:00
|
|
|
|
2019-07-08 17:34:44 +02:00
|
|
|
if factor and rate:
|
|
|
|
|
if _accurate_operator(factor, rate) == 'rate':
|
|
|
|
|
factor = None
|
|
|
|
|
else:
|
|
|
|
|
rate = None
|
|
|
|
|
if factor:
|
|
|
|
|
amount *= factor
|
|
|
|
|
elif rate:
|
|
|
|
|
amount /= rate
|
|
|
|
|
|
2015-07-29 17:48:28 +02:00
|
|
|
if to_uom.accurate_field == 'factor':
|
|
|
|
|
amount = amount / to_uom.factor
|
|
|
|
|
else:
|
|
|
|
|
amount = amount * to_uom.rate
|
|
|
|
|
|
|
|
|
|
if round:
|
2016-01-11 10:28:00 -03:00
|
|
|
amount = to_uom.round(amount)
|
2015-07-29 17:48:28 +02:00
|
|
|
|
2008-03-07 00:36:18 +01:00
|
|
|
return amount
|
|
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
@classmethod
|
2019-07-08 17:34:44 +02:00
|
|
|
def compute_price(cls, from_uom, price, to_uom, factor=None, rate=None):
|
2008-03-07 00:36:18 +01:00
|
|
|
"""
|
2010-03-11 15:29:21 +01:00
|
|
|
Convert price for given uom's.
|
2019-07-08 17:34:44 +02:00
|
|
|
|
|
|
|
|
When converting between uom's from different categories the factor and
|
|
|
|
|
rate provide the ratio to use to convert between the category's base
|
|
|
|
|
uom's.
|
2008-03-07 00:36:18 +01:00
|
|
|
"""
|
2015-07-29 17:48:28 +02:00
|
|
|
if not price or (from_uom is None and to_uom is None):
|
2008-03-07 00:36:18 +01:00
|
|
|
return price
|
2015-07-29 17:48:28 +02:00
|
|
|
if from_uom is None:
|
|
|
|
|
raise ValueError("missing from_uom")
|
|
|
|
|
if to_uom is None:
|
|
|
|
|
raise ValueError("missing to_uom")
|
2010-03-11 15:29:21 +01:00
|
|
|
if from_uom.category.id != to_uom.category.id:
|
2019-07-08 17:34:44 +02:00
|
|
|
if not factor and not rate:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
"cannot convert between %s and %s without a factor or rate"
|
2015-07-29 17:48:28 +02:00
|
|
|
% (from_uom.category.name, to_uom.category.name))
|
2019-07-08 17:34:44 +02:00
|
|
|
elif factor or rate:
|
|
|
|
|
raise ValueError("factor and rate not allow for same category")
|
2015-07-29 17:48:28 +02:00
|
|
|
|
2019-07-08 17:34:44 +02:00
|
|
|
format_ = '%%.%df' % uom_conversion_digits[1]
|
2008-05-13 13:08:29 +02:00
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
if from_uom.accurate_field == 'factor':
|
2019-07-08 17:34:44 +02:00
|
|
|
new_price = price / Decimal(format_ % from_uom.factor)
|
2008-03-07 00:36:18 +01:00
|
|
|
else:
|
2019-07-08 17:34:44 +02:00
|
|
|
new_price = price * Decimal(format_ % from_uom.rate)
|
|
|
|
|
|
|
|
|
|
if factor and rate:
|
|
|
|
|
if _accurate_operator(factor, rate) == 'rate':
|
|
|
|
|
factor = None
|
|
|
|
|
else:
|
|
|
|
|
rate = None
|
|
|
|
|
if factor:
|
|
|
|
|
new_price /= Decimal(factor)
|
|
|
|
|
elif rate:
|
|
|
|
|
new_price *= Decimal(rate)
|
2008-03-07 00:36:18 +01:00
|
|
|
|
2012-09-24 22:18:00 +02:00
|
|
|
if to_uom.accurate_field == 'factor':
|
2019-07-08 17:34:44 +02:00
|
|
|
new_price = new_price * Decimal(format_ % to_uom.factor)
|
2008-03-07 00:36:18 +01:00
|
|
|
else:
|
2019-07-08 17:34:44 +02:00
|
|
|
new_price = new_price / Decimal(format_ % to_uom.rate)
|
2008-03-07 00:36:18 +01:00
|
|
|
|
2008-03-14 14:08:26 +01:00
|
|
|
return new_price
|
2017-02-02 12:11:10 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _round(uom, number, func=round):
|
2019-10-11 22:23:58 +02:00
|
|
|
if not number:
|
|
|
|
|
# Avoid unnecessary computation
|
|
|
|
|
return number
|
2017-02-02 12:11:10 +01:00
|
|
|
precision = uom.rounding
|
|
|
|
|
# Convert precision into an integer greater than 1 to avoid precision lost.
|
|
|
|
|
# This works for most cases because rounding is often: n * 10**i
|
|
|
|
|
if precision < 1:
|
|
|
|
|
exp = -floor(log10(precision))
|
|
|
|
|
factor = 10 ** exp
|
|
|
|
|
number *= factor
|
|
|
|
|
precision *= factor
|
|
|
|
|
else:
|
|
|
|
|
factor = 1
|
|
|
|
|
# Divide by factor which is an integer to avoid precision lost due to
|
|
|
|
|
# multiplication by float < 1.
|
|
|
|
|
# example:
|
|
|
|
|
# >>> 3 * 0.1
|
|
|
|
|
# 0.30000000000000004
|
|
|
|
|
# >>> 3 / 10.
|
|
|
|
|
# 0.3
|
|
|
|
|
return func(number / precision) * precision / factor
|
2019-07-08 17:34:44 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _accurate_operator(factor, rate):
|
|
|
|
|
lengths = {}
|
|
|
|
|
for name, value in [('rate', rate), ('factor', factor)]:
|
|
|
|
|
format_ = '%%.%df' % uom_conversion_digits[1]
|
|
|
|
|
lengths[name] = len((format_ % value).split('.')[1].rstrip('0'))
|
|
|
|
|
if lengths['rate'] < lengths['factor']:
|
|
|
|
|
return 'rate'
|
|
|
|
|
elif lengths['factor'] < lengths['rate']:
|
|
|
|
|
return 'factor'
|
|
|
|
|
elif factor >= 1.0:
|
|
|
|
|
return 'factor'
|
|
|
|
|
else:
|
|
|
|
|
return 'rate'
|