Files
tryton/currency.py
T
Cédric Krier c7a612e72c Ensure precision is great enough when rounding
The quantize method raise InvalidOperation if the length of the coefficient
after the quantize operation would be greater than precision.

issue6534
review255701002
2019-08-07 19:18:48 +02:00

260 lines
8.9 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import datetime
from decimal import Decimal, ROUND_HALF_EVEN, localcontext
from sql import Window
from sql.functions import NthValue
from trytond.i18n import gettext
from trytond.model import (
ModelView, ModelSQL, DeactivableMixin, fields, Unique, Check)
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.rpc import RPC
from trytond.pyson import Eval
from .exceptions import RateError
__all__ = ['Currency', 'Rate']
class Currency(DeactivableMixin, ModelSQL, ModelView):
'Currency'
__name__ = 'currency.currency'
name = fields.Char('Name', required=True, translate=True,
help="The main identifier of the currency.")
symbol = fields.Char('Symbol', size=10, required=True,
help="The symbol used for currency formating.")
code = fields.Char('Code', size=3, required=True,
help="The 3 chars ISO currency code.")
numeric_code = fields.Char('Numeric Code', size=3,
help="The 3 digits ISO currency code.")
rate = fields.Function(fields.Numeric('Current rate', digits=(12, 6)),
'get_rate')
rates = fields.One2Many('currency.currency.rate', 'currency', 'Rates',
help="Add floating exchange rates for the currency.")
rounding = fields.Numeric('Rounding factor', required=True,
digits=(12, Eval('digits', 6)), depends=['digits'],
help="The minimum amount which can be represented in this currency.")
digits = fields.Integer("Digits", required=True,
help="The number of digits to display after the decimal separator.")
@classmethod
def __setup__(cls):
super(Currency, cls).__setup__()
cls._order.insert(0, ('code', 'ASC'))
cls.__rpc__.update({
'compute': RPC(instantiate=slice(0, 3, 2)),
})
@classmethod
def __register__(cls, module_name):
pool = Pool()
Data = pool.get('ir.model.data')
data = Data.__table__()
cursor = Transaction().connection.cursor()
super(Currency, cls).__register__(module_name)
table_h = cls.__table_handler__(module_name)
# Migration from 4.6: removal of monetary
for col in [
'mon_grouping', 'mon_decimal_point',
'p_sign_posn', 'n_sign_posn']:
table_h.not_null_action(col, 'remove')
# Migration from 5.2: remove country data
cursor.execute(*data.delete(where=(data.module == 'currency')
& (data.model == cls.__name__)))
@staticmethod
def default_rounding():
return Decimal('0.01')
@staticmethod
def default_digits():
return 2
@classmethod
def check_xml_record(cls, records, values):
return True
@classmethod
def search_global(cls, text):
for record, rec_name, icon in super(Currency, cls).search_global(text):
icon = icon or 'tryton-currency'
yield record, rec_name, icon
@classmethod
def search_rec_name(cls, name, clause):
currencies = None
field = None
for field in ('code', 'numeric_code'):
currencies = cls.search([(field,) + tuple(clause[1:])], limit=1)
if currencies:
break
if currencies:
return [(field,) + tuple(clause[1:])]
return [(cls._rec_name,) + tuple(clause[1:])]
@fields.depends('rates')
def on_change_with_rate(self):
now = datetime.date.today()
closer = datetime.date.min
res = Decimal('0.0')
for rate in self.rates or []:
date = getattr(rate, 'date', None) or now
if date <= now and date > closer:
res = rate.rate
closer = date
return res
@staticmethod
def get_rate(currencies, name):
'''
Return the rate at the date from the context or the current date
'''
Rate = Pool().get('currency.currency.rate')
Date = Pool().get('ir.date')
res = {}
date = Transaction().context.get('date', Date.today())
for currency in currencies:
rates = Rate.search([
('currency', '=', currency.id),
('date', '<=', date),
], limit=1, order=[('date', 'DESC')])
if rates:
res[currency.id] = rates[0].id
else:
res[currency.id] = 0
rate_ids = [x for x in res.values() if x]
rates = Rate.browse(rate_ids)
id2rate = {}
for rate in rates:
id2rate[rate.id] = rate
for currency_id in res.keys():
if res[currency_id]:
res[currency_id] = id2rate[res[currency_id]].rate
return res
def round(self, amount, rounding=ROUND_HALF_EVEN):
'Round the amount depending of the currency'
with localcontext() as ctx:
ctx.prec = max(ctx.prec, (amount / self.rounding).adjusted() + 1)
# Divide and multiple by rounding for case rounding is not 10En
result = (amount / self.rounding).quantize(Decimal('1.'),
rounding=rounding) * self.rounding
return Decimal(result)
def is_zero(self, amount):
'Return True if the amount can be considered as zero for the currency'
return abs(self.round(amount)) < self.rounding
@classmethod
def compute(cls, from_currency, amount, to_currency, round=True):
'''
Take a currency and an amount
Return the amount to the new currency
Use the rate of the date of the context or the current date
'''
Date = Pool().get('ir.date')
Lang = Pool().get('ir.lang')
from_currency = cls(int(from_currency))
to_currency = cls(int(to_currency))
if to_currency == from_currency:
if round:
return to_currency.round(amount)
else:
return amount
if (not from_currency.rate) or (not to_currency.rate):
date = Transaction().context.get('date', Date.today())
if not from_currency.rate:
name = from_currency.name
else:
name = to_currency.name
lang = Lang.get()
raise RateError(gettext('currency.msg_no_rate',
currency=name,
date=lang.strftime(date)))
if round:
return to_currency.round(
amount * to_currency.rate / from_currency.rate)
else:
return amount * to_currency.rate / from_currency.rate
@classmethod
def currency_rate_sql(cls):
"Return a SQL query with currency, rate, start_date and end_date"
pool = Pool()
Rate = pool.get('currency.currency.rate')
transaction = Transaction()
database = transaction.database
rate = Rate.__table__()
if database.has_window_functions():
window = Window(
[rate.currency],
order_by=[rate.date.asc],
frame='ROWS', start=0, end=1)
# Use NthValue instead of LastValue to get NULL for the last row
end_date = NthValue(rate.date, 2, window=window)
else:
next_rate = Rate.__table__()
end_date = next_rate.select(
next_rate.date,
where=(next_rate.currency == rate.currency)
& (next_rate.date > rate.date),
order_by=[next_rate.date.asc],
limit=1)
query = (rate
.select(
rate.currency.as_('currency'),
rate.rate.as_('rate'),
rate.date.as_('start_date'),
end_date.as_('end_date'),
))
return query
class Rate(ModelSQL, ModelView):
'Rate'
__name__ = 'currency.currency.rate'
date = fields.Date('Date', required=True, select=True,
help="From when the rate applies.")
rate = fields.Numeric('Rate', digits=(12, 6), required=1,
help="The floating exchange rate used to convert the currency.")
currency = fields.Many2One('currency.currency', 'Currency',
ondelete='CASCADE',
help="The currency on which the rate applies.")
@classmethod
def __setup__(cls):
super(Rate, cls).__setup__()
t = cls.__table__()
cls._sql_constraints = [
('date_currency_uniq', Unique(t, t.date, t.currency),
'currency.msg_currency_unique_rate_date'),
('check_currency_rate', Check(t, t.rate >= 0),
'currency.msg_currency_rate_positive'),
]
cls._order.insert(0, ('date', 'DESC'))
@staticmethod
def default_date():
Date = Pool().get('ir.date')
return Date.today()
@classmethod
def check_xml_record(cls, records, values):
return True
def get_rec_name(self, name):
return str(self.date)