#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