628333cffcdba446a1f4e1c746e43bc3e109a04f
[odoo/odoo.git] / openerp / addons / base / res / res_currency.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 import re
23 import time
24
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 _
29
30 CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?')
31
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)
35
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)
38
39     def _get_current_rate(self, cr, uid, ids, raise_on_no_rate=True, context=None):
40         if context is None:
41             context = {}
42         res = {}
43
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'
49         for id in ids:
50             cr.execute('SELECT rate FROM res_currency_rate '
51                        'WHERE currency_id = %s '
52                          'AND name <= %s '
53                          'AND currency_rate_type_id ' + operator + ' %s '
54                        'ORDER BY name desc LIMIT 1',
55                        (id, date, currency_rate_type))
56             if cr.rowcount:
57                 res[id] = cr.fetchone()[0]
58             elif not raise_on_no_rate:
59                 res[id] = 0
60             else:
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)))
63         return res
64
65     _name = "res.currency"
66     _description = "Currency"
67     _columns = {
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.'),
73
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.")
85     }
86     _defaults = {
87         'active': 1,
88         'position' : 'after',
89         'rounding': 0.01,
90         'accuracy': 4,
91         'company_id': False,
92     }
93     _sql_constraints = [
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!'),
99     ]
100     _order = "name"
101
102     def init(self, cr):
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
111                           ON res_currency
112                           (name, (COALESCE(company_id,-1)))""")
113
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')
117         values = res
118         if not isinstance(values, list):
119             values = [values]
120         for r in values:
121             if r.__contains__('rate_ids'):
122                 rates=r['rate_ids']
123                 if rates:
124                     currency_date = currency_rate_obj.read(cr, user, rates[0], ['name'])['name']
125                     r['date'] = currency_date
126         return res
127
128     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
129         if not args:
130             args = []
131         results = super(res_currency,self)\
132             .name_search(cr, user, name, args, operator=operator, context=context, limit=limit)
133         if not results:
134             name_match = CURRENCY_DISPLAY_PATTERN.match(name)
135             if name_match:
136                 results = super(res_currency,self)\
137                     .name_search(cr, user, name_match.group(1), args, operator=operator, context=context, limit=limit)
138         return results
139
140     def name_get(self, cr, uid, ids, context=None):
141         if not ids:
142             return []
143         if isinstance(ids, (int, long)):
144             ids = [ids]
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]
147
148     def round(self, cr, uid, currency, amount):
149         """Return ``amount`` rounded  according to ``currency``'s
150            rounding rules.
151
152            :param browse_record currency: currency for which we are rounding
153            :param float amount: the amount to round
154            :return: rounded float
155         """
156         return float_round(amount, precision_rounding=currency.rounding)
157
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!
163
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.
169
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.
176         """
177         return float_compare(amount1, amount2, precision_rounding=currency.rounding)
178
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.
182
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.
187
188            :param browse_record currency: currency for which we are rounding
189            :param float amount: amount to compare with currency's zero
190         """
191         return float_is_zero(amount, precision_rounding=currency.rounding)
192
193     def _get_conversion_rate(self, cr, uid, from_currency, to_currency, context=None):
194         if context is None:
195             context = {}
196         ctx = context.copy()
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)
199
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)
202
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
207             else:
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
213
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):
216         if not context:
217             context = {}
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):
226             if round:
227                 return self.round(cr, uid, to_currency, from_amount)
228             else:
229                 return from_amount
230         else:
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)
233             if round:
234                 return self.round(cr, uid, to_currency, from_amount * rate)
235             else:
236                 return from_amount * rate
237
238 class res_currency_rate_type(osv.osv):
239     _name = "res.currency.rate.type"
240     _description = "Currency Rate Type"
241     _columns = {
242         'name': fields.char('Name', required=True, translate=True),
243     }
244
245 class res_currency_rate(osv.osv):
246     _name = "res.currency.rate"
247     _description = "Currency Rate"
248
249     _columns = {
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"),
254     }
255     _defaults = {
256         'name': lambda *a: time.strftime('%Y-%m-%d'),
257     }
258     _order = "name desc"
259
260 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
261