1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
24 from osv import fields, osv
27 from tools import float_round, float_is_zero, float_compare
28 from tools.translate import _
30 CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?')
32 class res_currency(osv.osv):
33 def _current_rate(self, cr, uid, ids, name, arg, context=None):
38 date = context['date']
40 date = time.strftime('%Y-%m-%d')
41 date = date or time.strftime('%Y-%m-%d')
42 # Convert False values to None ...
43 currency_rate_type = context.get('currency_rate_type_id') or None
44 # ... and use 'is NULL' instead of '= some-id'.
45 operator = '=' if currency_rate_type else 'is'
47 cr.execute("SELECT currency_id, rate FROM res_currency_rate WHERE currency_id = %s AND name <= %s AND currency_rate_type_id " + operator +" %s ORDER BY name desc LIMIT 1" ,(id, date, currency_rate_type))
49 id, rate = cr.fetchall()[0]
54 _name = "res.currency"
55 _description = "Currency"
57 # Note: 'code' column was removed as of v6.0, the 'name' should now hold the ISO code.
58 'name': fields.char('Currency', size=32, required=True, help="Currency Code (ISO 4217)"),
59 'symbol': fields.char('Symbol', size=3, help="Currency sign, to be used when printing amounts."),
60 'rate': fields.function(_current_rate, string='Current Rate', digits=(12,6),
61 help='The rate of the currency to the currency of rate 1.'),
62 'rate_ids': fields.one2many('res.currency.rate', 'currency_id', 'Rates'),
63 'accuracy': fields.integer('Computational Accuracy'),
64 'rounding': fields.float('Rounding Factor', digits=(12,6)),
65 'active': fields.boolean('Active'),
66 'company_id':fields.many2one('res.company', 'Company'),
67 'date': fields.date('Date'),
68 'base': fields.boolean('Base'),
69 'position': fields.selection([('after','After Amount'),('before','Before Amount')], 'Symbol Position', help="Determines where the currency symbol should be placed after or before the amount.")
72 'active': lambda *a: 1,
78 # this constraint does not cover all cases due to SQL NULL handling for company_id,
79 # so it is complemented with a unique index (see below). The constraint and index
80 # share the same prefix so that IntegrityError triggered by the index will be caught
81 # and reported to the user with the constraint's error message.
82 ('unique_name_company_id', 'unique (name, company_id)', 'The currency code must be unique per company!'),
87 # CONSTRAINT/UNIQUE INDEX on (name,company_id)
88 # /!\ The unique constraint 'unique_name_company_id' is not sufficient, because SQL92
89 # only support field names in constraint definitions, and we need a function here:
90 # we need to special-case company_id to treat all NULL company_id as equal, otherwise
91 # we would allow duplicate "global" currencies (all having company_id == NULL)
92 cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'res_currency_unique_name_company_id_idx'""")
94 cr.execute("""CREATE UNIQUE INDEX res_currency_unique_name_company_id_idx
96 (name, (COALESCE(company_id,-1)))""")
98 def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
99 res = super(res_currency, self).read(cr, user, ids, fields, context, load)
100 currency_rate_obj = self.pool.get('res.currency.rate')
102 if not isinstance(values, (list)):
105 if r.__contains__('rate_ids'):
108 currency_date = currency_rate_obj.read(cr, user, rates[0], ['name'])['name']
109 r['date'] = currency_date
112 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
115 results = super(res_currency,self)\
116 .name_search(cr, user, name, args, operator=operator, context=context, limit=limit)
118 name_match = CURRENCY_DISPLAY_PATTERN.match(name)
120 results = super(res_currency,self)\
121 .name_search(cr, user, name_match.group(1), args, operator=operator, context=context, limit=limit)
124 def name_get(self, cr, uid, ids, context=None):
127 if isinstance(ids, (int, long)):
129 reads = self.read(cr, uid, ids, ['name','symbol'], context=context, load='_classic_write')
130 return [(x['id'], tools.ustr(x['name']) + (x['symbol'] and (' (' + tools.ustr(x['symbol']) + ')') or '')) for x in reads]
132 def round(self, cr, uid, currency, amount):
133 """Return ``amount`` rounded according to ``currency``'s
136 :param browse_record currency: currency for which we are rounding
137 :param float amount: the amount to round
138 :return: rounded float
140 return float_round(amount, precision_rounding=currency.rounding)
142 def compare_amounts(self, cr, uid, currency, amount1, amount2):
143 """Compare ``amount1`` and ``amount2`` after rounding them according to the
144 given currency's precision..
145 An amount is considered lower/greater than another amount if their rounded
146 value is different. This is not the same as having a non-zero difference!
148 For example 1.432 and 1.431 are equal at 2 digits precision,
149 so this method would return 0.
150 However 0.006 and 0.002 are considered different (returns 1) because
151 they respectively round to 0.01 and 0.0, even though
152 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.
154 :param browse_record currency: currency for which we are rounding
155 :param float amount1: first amount to compare
156 :param float amount2: second amount to compare
157 :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than,
158 equal to, or greater than ``amount2``, according to
159 ``currency``'s rounding.
161 return float_compare(amount1, amount2, precision_rounding=currency.rounding)
163 def is_zero(self, cr, uid, currency, amount):
164 """Returns true if ``amount`` is small enough to be treated as
165 zero according to ``currency``'s rounding rules.
167 Warning: ``is_zero(amount1-amount2)`` is not always equivalent to
168 ``compare_amounts(amount1,amount2) == 0``, as the former will round after
169 computing the difference, while the latter will round before, giving
170 different results for e.g. 0.006 and 0.002 at 2 digits precision.
172 :param browse_record currency: currency for which we are rounding
173 :param float amount: amount to compare with currency's zero
175 return float_is_zero(amount, precision_rounding=currency.rounding)
177 def _get_conversion_rate(self, cr, uid, from_currency, to_currency, context=None):
181 ctx.update({'currency_rate_type_id': ctx.get('currency_rate_type_from')})
182 from_currency = self.browse(cr, uid, from_currency.id, context=ctx)
184 ctx.update({'currency_rate_type_id': ctx.get('currency_rate_type_to')})
185 to_currency = self.browse(cr, uid, to_currency.id, context=ctx)
187 if from_currency.rate == 0 or to_currency.rate == 0:
188 date = context.get('date', time.strftime('%Y-%m-%d'))
189 if from_currency.rate == 0:
190 currency_symbol = from_currency.symbol
192 currency_symbol = to_currency.symbol
193 raise osv.except_osv(_('Error'), _('No rate found \n' \
194 'for the currency: %s \n' \
195 'at the date: %s') % (currency_symbol, date))
196 return to_currency.rate/from_currency.rate
198 def compute(self, cr, uid, from_currency_id, to_currency_id, from_amount,
199 round=True, currency_rate_type_from=False, currency_rate_type_to=False, context=None):
202 if not from_currency_id:
203 from_currency_id = to_currency_id
204 if not to_currency_id:
205 to_currency_id = from_currency_id
206 xc = self.browse(cr, uid, [from_currency_id,to_currency_id], context=context)
207 from_currency = (xc[0].id == from_currency_id and xc[0]) or xc[1]
208 to_currency = (xc[0].id == to_currency_id and xc[0]) or xc[1]
209 if (to_currency_id == from_currency_id) and (currency_rate_type_from == currency_rate_type_to):
211 return self.round(cr, uid, to_currency, from_amount)
215 context.update({'currency_rate_type_from': currency_rate_type_from, 'currency_rate_type_to': currency_rate_type_to})
216 rate = self._get_conversion_rate(cr, uid, from_currency, to_currency, context=context)
218 return self.round(cr, uid, to_currency, from_amount * rate)
220 return (from_amount * rate)
224 class res_currency_rate_type(osv.osv):
225 _name = "res.currency.rate.type"
226 _description = "Currency Rate Type"
228 'name': fields.char('Name', size=64, required=True, translate=True),
231 res_currency_rate_type()
233 class res_currency_rate(osv.osv):
234 _name = "res.currency.rate"
235 _description = "Currency Rate"
238 'name': fields.date('Date', required=True, select=True),
239 'rate': fields.float('Rate', digits=(12,6), help='The rate of the currency to the currency of rate 1'),
240 'currency_id': fields.many2one('res.currency', 'Currency', readonly=True),
241 'currency_rate_type_id': fields.many2one('res.currency.rate.type', 'Currency Rate Type', help="Allow you to define your own currency rate types, like 'Average' or 'Year to Date'. Leave empty if you simply want to use the normal 'spot' rate type"),
244 'name': lambda *a: time.strftime('%Y-%m-%d'),
250 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: