The quantize method raise InvalidOperation if the length of the coefficient after the quantize operation would be greater than precision. issue6534 review255701002
260 lines
8.9 KiB
Python
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)
|