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 ##############################################################################
25 from openerp import tools
26 from openerp.osv import fields, osv
27 from openerp.tools import float_round, float_is_zero, float_compare
28 from openerp.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):
34 return self._get_current_rate(cr, uid, ids, context=context)
36 def _current_rate_silent(self, cr, uid, ids, name, arg, context=None):
37 return self._get_current_rate(cr, uid, ids, raise_on_no_rate=False, context=context)
39 def _get_current_rate(self, cr, uid, ids, raise_on_no_rate=True, context=None):
44 date = context.get('date') or time.strftime('%Y-%m-%d')
45 # Convert False values to None ...
46 currency_rate_type = context.get('currency_rate_type_id') or None
47 # ... and use 'is NULL' instead of '= some-id'.
48 operator = '=' if currency_rate_type else 'is'
50 cr.execute('SELECT rate FROM res_currency_rate '
51 'WHERE currency_id = %s '
53 'AND currency_rate_type_id ' + operator + ' %s '
54 'ORDER BY name desc LIMIT 1',
55 (id, date, currency_rate_type))
57 res[id] = cr.fetchone()[0]
58 elif not raise_on_no_rate:
61 currency = self.browse(cr, uid, id, context=context)
62 raise osv.except_osv(_('Error!'),_("No currency rate associated for currency '%s' for the given period" % (currency.name)))
65 _name = "res.currency"
66 _description = "Currency"
68 # Note: 'code' column was removed as of v6.0, the 'name' should now hold the ISO code.
69 'name': fields.char('Currency', size=3, required=True, help="Currency Code (ISO 4217)"),
70 'symbol': fields.char('Symbol', size=4, help="Currency sign, to be used when printing amounts."),
71 'rate': fields.function(_current_rate, string='Current Rate', digits=(12,6),
72 help='The rate of the currency to the currency of rate 1.'),
74 # Do not use for computation ! Same as rate field with silent failing
75 'rate_silent': fields.function(_current_rate_silent, string='Current Rate', digits=(12,6),
76 help='The rate of the currency to the currency of rate 1 (0 if no rate defined).'),
77 'rate_ids': fields.one2many('res.currency.rate', 'currency_id', 'Rates'),
78 'accuracy': fields.integer('Computational Accuracy'),
79 'rounding': fields.float('Rounding Factor', digits=(12,6)),
80 'active': fields.boolean('Active'),
81 'company_id':fields.many2one('res.company', 'Company'),
82 'date': fields.date('Date'),
83 'base': fields.boolean('Base'),
84 '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.")
94 # this constraint does not cover all cases due to SQL NULL handling for company_id,
95 # so it is complemented with a unique index (see below). The constraint and index
96 # share the same prefix so that IntegrityError triggered by the index will be caught
97 # and reported to the user with the constraint's error message.
98 ('unique_name_company_id', 'unique (name, company_id)', 'The currency code must be unique per company!'),
103 # CONSTRAINT/UNIQUE INDEX on (name,company_id)
104 # /!\ The unique constraint 'unique_name_company_id' is not sufficient, because SQL92
105 # only support field names in constraint definitions, and we need a function here:
106 # we need to special-case company_id to treat all NULL company_id as equal, otherwise
107 # we would allow duplicate "global" currencies (all having company_id == NULL)
108 cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'res_currency_unique_name_company_id_idx'""")
109 if not cr.fetchone():
110 cr.execute("""CREATE UNIQUE INDEX res_currency_unique_name_company_id_idx
112 (name, (COALESCE(company_id,-1)))""")
114 def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
115 res = super(res_currency, self).read(cr, user, ids, fields, context, load)
116 currency_rate_obj = self.pool.get('res.currency.rate')
118 if not isinstance(values, list):
121 if r.__contains__('rate_ids'):
124 currency_date = currency_rate_obj.read(cr, user, rates[0], ['name'])['name']
125 r['date'] = currency_date
128 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
131 results = super(res_currency,self)\
132 .name_search(cr, user, name, args, operator=operator, context=context, limit=limit)
134 name_match = CURRENCY_DISPLAY_PATTERN.match(name)
136 results = super(res_currency,self)\
137 .name_search(cr, user, name_match.group(1), args, operator=operator, context=context, limit=limit)
140 def name_get(self, cr, uid, ids, context=None):
143 if isinstance(ids, (int, long)):
145 reads = self.read(cr, uid, ids, ['name','symbol'], context=context, load='_classic_write')
146 return [(x['id'], tools.ustr(x['name'])) for x in reads]
148 def round(self, cr, uid, currency, amount):
149 """Return ``amount`` rounded according to ``currency``'s
152 :param browse_record currency: currency for which we are rounding
153 :param float amount: the amount to round
154 :return: rounded float
156 return float_round(amount, precision_rounding=currency.rounding)
158 def compare_amounts(self, cr, uid, currency, amount1, amount2):
159 """Compare ``amount1`` and ``amount2`` after rounding them according to the
160 given currency's precision..
161 An amount is considered lower/greater than another amount if their rounded
162 value is different. This is not the same as having a non-zero difference!
164 For example 1.432 and 1.431 are equal at 2 digits precision,
165 so this method would return 0.
166 However 0.006 and 0.002 are considered different (returns 1) because
167 they respectively round to 0.01 and 0.0, even though
168 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.
170 :param browse_record currency: currency for which we are rounding
171 :param float amount1: first amount to compare
172 :param float amount2: second amount to compare
173 :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than,
174 equal to, or greater than ``amount2``, according to
175 ``currency``'s rounding.
177 return float_compare(amount1, amount2, precision_rounding=currency.rounding)
179 def is_zero(self, cr, uid, currency, amount):
180 """Returns true if ``amount`` is small enough to be treated as
181 zero according to ``currency``'s rounding rules.
183 Warning: ``is_zero(amount1-amount2)`` is not always equivalent to
184 ``compare_amounts(amount1,amount2) == 0``, as the former will round after
185 computing the difference, while the latter will round before, giving
186 different results for e.g. 0.006 and 0.002 at 2 digits precision.
188 :param browse_record currency: currency for which we are rounding
189 :param float amount: amount to compare with currency's zero
191 return float_is_zero(amount, precision_rounding=currency.rounding)
193 def _get_conversion_rate(self, cr, uid, from_currency, to_currency, context=None):
197 ctx.update({'currency_rate_type_id': ctx.get('currency_rate_type_from')})
198 from_currency = self.browse(cr, uid, from_currency.id, context=ctx)
200 ctx.update({'currency_rate_type_id': ctx.get('currency_rate_type_to')})
201 to_currency = self.browse(cr, uid, to_currency.id, context=ctx)
203 if from_currency.rate == 0 or to_currency.rate == 0:
204 date = context.get('date', time.strftime('%Y-%m-%d'))
205 if from_currency.rate == 0:
206 currency_symbol = from_currency.symbol
208 currency_symbol = to_currency.symbol
209 raise osv.except_osv(_('Error'), _('No rate found \n' \
210 'for the currency: %s \n' \
211 'at the date: %s') % (currency_symbol, date))
212 return to_currency.rate/from_currency.rate
214 def compute(self, cr, uid, from_currency_id, to_currency_id, from_amount,
215 round=True, currency_rate_type_from=False, currency_rate_type_to=False, context=None):
218 if not from_currency_id:
219 from_currency_id = to_currency_id
220 if not to_currency_id:
221 to_currency_id = from_currency_id
222 xc = self.browse(cr, uid, [from_currency_id,to_currency_id], context=context)
223 from_currency = (xc[0].id == from_currency_id and xc[0]) or xc[1]
224 to_currency = (xc[0].id == to_currency_id and xc[0]) or xc[1]
225 if (to_currency_id == from_currency_id) and (currency_rate_type_from == currency_rate_type_to):
227 return self.round(cr, uid, to_currency, from_amount)
231 context.update({'currency_rate_type_from': currency_rate_type_from, 'currency_rate_type_to': currency_rate_type_to})
232 rate = self._get_conversion_rate(cr, uid, from_currency, to_currency, context=context)
234 return self.round(cr, uid, to_currency, from_amount * rate)
236 return from_amount * rate
238 class res_currency_rate_type(osv.osv):
239 _name = "res.currency.rate.type"
240 _description = "Currency Rate Type"
242 'name': fields.char('Name', required=True, translate=True),
245 class res_currency_rate(osv.osv):
246 _name = "res.currency.rate"
247 _description = "Currency Rate"
250 'name': fields.datetime('Date', required=True, select=True),
251 'rate': fields.float('Rate', digits=(12, 6), help='The rate of the currency to the currency of rate 1'),
252 'currency_id': fields.many2one('res.currency', 'Currency', readonly=True),
253 '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"),
256 'name': lambda *a: time.strftime('%Y-%m-%d'),
260 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: