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):
34 def _current_rate(self, cr, uid, ids, name, arg, context=None):
35 return self._get_current_rate(cr, uid, ids, name, arg, context=context)
37 def _get_current_rate(self, cr, uid, ids, name, arg, context=None):
42 date = context['date']
44 date = time.strftime('%Y-%m-%d')
45 date = date or time.strftime('%Y-%m-%d')
46 # Convert False values to None ...
47 currency_rate_type = context.get('currency_rate_type_id') or None
48 # ... and use 'is NULL' instead of '= some-id'.
49 operator = '=' if currency_rate_type else 'is'
51 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))
53 id, rate = cr.fetchall()[0]
56 raise osv.except_osv(_('Error!'),_("No currency rate associated for currency %d for the given period" % (id)))
58 _name = "res.currency"
59 _description = "Currency"
61 # Note: 'code' column was removed as of v6.0, the 'name' should now hold the ISO code.
62 'name': fields.char('Currency', size=32, required=True, help="Currency Code (ISO 4217)"),
63 'symbol': fields.char('Symbol', size=4, help="Currency sign, to be used when printing amounts."),
64 'rate': fields.function(_current_rate, string='Current Rate', digits=(12,6),
65 help='The rate of the currency to the currency of rate 1.'),
66 'rate_ids': fields.one2many('res.currency.rate', 'currency_id', 'Rates'),
67 'accuracy': fields.integer('Computational Accuracy'),
68 'rounding': fields.float('Rounding Factor', digits=(12,6)),
69 'active': fields.boolean('Active'),
70 'company_id':fields.many2one('res.company', 'Company'),
71 'date': fields.date('Date'),
72 'base': fields.boolean('Base'),
73 '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.")
83 # this constraint does not cover all cases due to SQL NULL handling for company_id,
84 # so it is complemented with a unique index (see below). The constraint and index
85 # share the same prefix so that IntegrityError triggered by the index will be caught
86 # and reported to the user with the constraint's error message.
87 ('unique_name_company_id', 'unique (name, company_id)', 'The currency code must be unique per company!'),
92 # CONSTRAINT/UNIQUE INDEX on (name,company_id)
93 # /!\ The unique constraint 'unique_name_company_id' is not sufficient, because SQL92
94 # only support field names in constraint definitions, and we need a function here:
95 # we need to special-case company_id to treat all NULL company_id as equal, otherwise
96 # we would allow duplicate "global" currencies (all having company_id == NULL)
97 cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'res_currency_unique_name_company_id_idx'""")
99 cr.execute("""CREATE UNIQUE INDEX res_currency_unique_name_company_id_idx
101 (name, (COALESCE(company_id,-1)))""")
103 def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
104 res = super(res_currency, self).read(cr, user, ids, fields, context, load)
105 currency_rate_obj = self.pool.get('res.currency.rate')
107 if not isinstance(values, list):
110 if r.__contains__('rate_ids'):
113 currency_date = currency_rate_obj.read(cr, user, rates[0], ['name'])['name']
114 r['date'] = currency_date
117 def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
120 results = super(res_currency,self)\
121 .name_search(cr, user, name, args, operator=operator, context=context, limit=limit)
123 name_match = CURRENCY_DISPLAY_PATTERN.match(name)
125 results = super(res_currency,self)\
126 .name_search(cr, user, name_match.group(1), args, operator=operator, context=context, limit=limit)
129 def name_get(self, cr, uid, ids, context=None):
132 if isinstance(ids, (int, long)):
134 reads = self.read(cr, uid, ids, ['name','symbol'], context=context, load='_classic_write')
135 return [(x['id'], tools.ustr(x['name'])) for x in reads]
137 def round(self, cr, uid, currency, amount):
138 """Return ``amount`` rounded according to ``currency``'s
141 :param browse_record currency: currency for which we are rounding
142 :param float amount: the amount to round
143 :return: rounded float
145 return float_round(amount, precision_rounding=currency.rounding)
147 def compare_amounts(self, cr, uid, currency, amount1, amount2):
148 """Compare ``amount1`` and ``amount2`` after rounding them according to the
149 given currency's precision..
150 An amount is considered lower/greater than another amount if their rounded
151 value is different. This is not the same as having a non-zero difference!
153 For example 1.432 and 1.431 are equal at 2 digits precision,
154 so this method would return 0.
155 However 0.006 and 0.002 are considered different (returns 1) because
156 they respectively round to 0.01 and 0.0, even though
157 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.
159 :param browse_record currency: currency for which we are rounding
160 :param float amount1: first amount to compare
161 :param float amount2: second amount to compare
162 :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than,
163 equal to, or greater than ``amount2``, according to
164 ``currency``'s rounding.
166 return float_compare(amount1, amount2, precision_rounding=currency.rounding)
168 def is_zero(self, cr, uid, currency, amount):
169 """Returns true if ``amount`` is small enough to be treated as
170 zero according to ``currency``'s rounding rules.
172 Warning: ``is_zero(amount1-amount2)`` is not always equivalent to
173 ``compare_amounts(amount1,amount2) == 0``, as the former will round after
174 computing the difference, while the latter will round before, giving
175 different results for e.g. 0.006 and 0.002 at 2 digits precision.
177 :param browse_record currency: currency for which we are rounding
178 :param float amount: amount to compare with currency's zero
180 return float_is_zero(amount, precision_rounding=currency.rounding)
182 def _get_conversion_rate(self, cr, uid, from_currency, to_currency, context=None):
186 ctx.update({'currency_rate_type_id': ctx.get('currency_rate_type_from')})
187 from_currency = self.browse(cr, uid, from_currency.id, context=ctx)
189 ctx.update({'currency_rate_type_id': ctx.get('currency_rate_type_to')})
190 to_currency = self.browse(cr, uid, to_currency.id, context=ctx)
192 if from_currency.rate == 0 or to_currency.rate == 0:
193 date = context.get('date', time.strftime('%Y-%m-%d'))
194 if from_currency.rate == 0:
195 currency_symbol = from_currency.symbol
197 currency_symbol = to_currency.symbol
198 raise osv.except_osv(_('Error'), _('No rate found \n' \
199 'for the currency: %s \n' \
200 'at the date: %s') % (currency_symbol, date))
201 return to_currency.rate/from_currency.rate
203 def compute(self, cr, uid, from_currency_id, to_currency_id, from_amount,
204 round=True, currency_rate_type_from=False, currency_rate_type_to=False, context=None):
207 if not from_currency_id:
208 from_currency_id = to_currency_id
209 if not to_currency_id:
210 to_currency_id = from_currency_id
211 xc = self.browse(cr, uid, [from_currency_id,to_currency_id], context=context)
212 from_currency = (xc[0].id == from_currency_id and xc[0]) or xc[1]
213 to_currency = (xc[0].id == to_currency_id and xc[0]) or xc[1]
214 if (to_currency_id == from_currency_id) and (currency_rate_type_from == currency_rate_type_to):
216 return self.round(cr, uid, to_currency, from_amount)
220 context.update({'currency_rate_type_from': currency_rate_type_from, 'currency_rate_type_to': currency_rate_type_to})
221 rate = self._get_conversion_rate(cr, uid, from_currency, to_currency, context=context)
223 return self.round(cr, uid, to_currency, from_amount * rate)
225 return from_amount * rate
229 class res_currency_rate_type(osv.osv):
230 _name = "res.currency.rate.type"
231 _description = "Currency Rate Type"
233 'name': fields.char('Name', size=64, required=True, translate=True),
236 res_currency_rate_type()
238 class res_currency_rate(osv.osv):
239 _name = "res.currency.rate"
240 _description = "Currency Rate"
243 'name': fields.date('Date', required=True, select=True),
244 'rate': fields.float('Rate', digits=(12,6), help='The rate of the currency to the currency of rate 1'),
245 'currency_id': fields.many2one('res.currency', 'Currency', readonly=True),
246 '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"),
249 'name': lambda *a: time.strftime('%Y-%m-%d'),
255 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: