Files
tryton/currency.py
T

273 lines
8.7 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
from trytond.model import ModelView, ModelSQL, fields
from trytond.tools import safe_eval, datetime_strftime
from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.rpc import RPC
__all__ = ['Currency', 'Rate']
class Currency(ModelSQL, ModelView):
'Currency'
__name__ = 'currency.currency'
name = fields.Char('Name', required=True, translate=True)
symbol = fields.Char('Symbol', size=10, required=True)
code = fields.Char('Code', size=3, required=True)
numeric_code = fields.Char('Numeric Code', size=3)
rate = fields.Function(fields.Numeric('Current rate', digits=(12, 6),
on_change_with=['rates']), 'get_rate')
rates = fields.One2Many('currency.currency.rate', 'currency', 'Rates')
rounding = fields.Numeric('Rounding factor', digits=(12, 6), required=True)
digits = fields.Integer('Display Digits', required=True)
active = fields.Boolean('Active')
# monetary formatting
mon_grouping = fields.Char('Grouping', required=True)
mon_decimal_point = fields.Char('Decimal Separator', required=True)
mon_thousands_sep = fields.Char('Thousands Separator')
p_sign_posn = fields.Integer('Positive Sign Position', required=True)
n_sign_posn = fields.Integer('Negative Sign Position', required=True)
positive_sign = fields.Char('Positive Sign')
negative_sign = fields.Char('Negative Sign')
p_cs_precedes = fields.Boolean('Positive Currency Symbol Precedes')
n_cs_precedes = fields.Boolean('Negative Currency Symbol Precedes')
p_sep_by_space = fields.Boolean('Positive Separate by Space')
n_sep_by_space = fields.Boolean('Negative Separate by Space')
@classmethod
def __setup__(cls):
super(Currency, cls).__setup__()
cls._order.insert(0, ('code', 'ASC'))
cls._error_messages.update({
'no_rate': ('No rate found for currency "%(currency)s" on '
'"%(date)s"'),
'invalid_mon_grouping': ('Invalid grouping "%(grouping)s" on '
' currency "%(currency)s".'),
})
cls.__rpc__.update({
'compute': RPC(),
})
@staticmethod
def default_active():
return True
@staticmethod
def default_rounding():
return Decimal('0.01')
@staticmethod
def default_digits():
return 2
@staticmethod
def default_mon_grouping():
return '[]'
@staticmethod
def default_mon_thousands_sep():
return ','
@staticmethod
def default_mon_decimal_point():
return '.'
@staticmethod
def default_p_sign_posn():
return 1
@staticmethod
def default_n_sign_posn():
return 1
@staticmethod
def default_negative_sign():
return '-'
@staticmethod
def default_positive_sign():
return ''
@staticmethod
def default_p_cs_precedes():
return True
@staticmethod
def default_n_cs_precedes():
return True
@staticmethod
def default_p_sep_by_space():
return False
@staticmethod
def default_n_sep_by_space():
return False
@classmethod
def validate(cls, currencies):
super(Currency, cls).validate(currencies)
for currency in currencies:
currency.check_mon_grouping()
def check_mon_grouping(self):
'''
Check if mon_grouping is list of numbers
'''
try:
grouping = safe_eval(self.mon_grouping)
for i in grouping:
if not isinstance(i, int):
raise ValueError
except Exception:
self.raise_user_error('invalid_mon_grouping', {
'grouping': self.mon_grouping,
'currency': self.rec_name,
})
@classmethod
def check_xml_record(cls, records, values):
return True
@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:])]
def on_change_with_rate(self):
now = datetime.date.today()
closer = datetime.date.min
res = Decimal('0.0')
for rate in self.rates or []:
if 'date' not in rate or 'rate' not in rate:
continue
if rate['date'] <= now and rate['date'] > closer:
res = rate['rate']
closer = rate['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
:param cursor: the database cursor
:param user: the user id
:param currency: a BrowseRecord of currency.currency
:param amout: a Decimal
:param rounding: the rounding option
:return: a Decimal
'''
return (amount / self.rounding).quantize(Decimal('1.'),
rounding=rounding) * self.rounding
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(from_currency.id)
to_currency = cls(to_currency.id)
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
languages = Lang.search([
('code', '=', Transaction().language),
])
cls.raise_user_error('no_rate', {
'currency': name,
'date': datetime_strftime(date, str(languages[0].date))
})
if round:
return to_currency.round(
amount * to_currency.rate / from_currency.rate)
else:
return amount * to_currency.rate / from_currency.rate
class Rate(ModelSQL, ModelView):
'Rate'
__name__ = 'currency.currency.rate'
_rec_name = 'date'
date = fields.Date('Date', required=True, select=True)
rate = fields.Numeric('Rate', digits=(12, 6), required=1)
currency = fields.Many2One('currency.currency', 'Currency',
ondelete='CASCADE',)
@classmethod
def __setup__(cls):
super(Rate, cls).__setup__()
cls._sql_constraints = [
('date_currency_uniq', 'UNIQUE(date, currency)',
'A currency can only have one rate by date.'),
('check_currency_rate', 'CHECK(rate >= 0)',
'The currency rate must greater than or equal to 0'),
]
cls._order.insert(0, ('date', 'DESC'))
@staticmethod
def default_date():
Date = Pool().get('ir.date')
return Date.today()
@classmethod
def check_xml_record(self, records, values):
return True