efe33f32fd94ece6167bf5867e04026f27c697d1
[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
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)
36
37     def _get_current_rate(self, cr, uid, ids, name, arg, context=None):
38         if context is None:
39             context = {}
40         res = {}
41         if 'date' in context:
42             date = context['date']
43         else:
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'
50         for id in ids:
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))
52             if cr.rowcount:
53                 id, rate = cr.fetchall()[0]
54                 res[id] = rate
55             else:
56                 raise osv.except_osv(_('Error!'),_("No currency rate associated for currency %d for the given period" % (id)))
57         return res
58     _name = "res.currency"
59     _description = "Currency"
60     _columns = {
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.")
74     }
75     _defaults = {
76         'active': 1,
77         'position' : 'after',
78         'rounding': 0.01,
79         'accuracy': 4,
80         'company_id': False,
81     }
82     _sql_constraints = [
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!'),
88     ]
89     _order = "name"
90
91     def init(self, cr):
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'""")
98         if not cr.fetchone():
99             cr.execute("""CREATE UNIQUE INDEX res_currency_unique_name_company_id_idx
100                           ON res_currency
101                           (name, (COALESCE(company_id,-1)))""")
102
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')
106         values = res
107         if not isinstance(values, list):
108             values = [values]
109         for r in values:
110             if r.__contains__('rate_ids'):
111                 rates=r['rate_ids']
112                 if rates:
113                     currency_date = currency_rate_obj.read(cr, user, rates[0], ['name'])['name']
114                     r['date'] = currency_date
115         return res
116
117     def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100):
118         if not args:
119             args = []
120         results = super(res_currency,self)\
121             .name_search(cr, user, name, args, operator=operator, context=context, limit=limit)
122         if not results:
123             name_match = CURRENCY_DISPLAY_PATTERN.match(name)
124             if name_match:
125                 results = super(res_currency,self)\
126                     .name_search(cr, user, name_match.group(1), args, operator=operator, context=context, limit=limit)
127         return results
128
129     def name_get(self, cr, uid, ids, context=None):
130         if not ids:
131             return []
132         if isinstance(ids, (int, long)):
133             ids = [ids]
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]
136
137     def round(self, cr, uid, currency, amount):
138         """Return ``amount`` rounded  according to ``currency``'s
139            rounding rules.
140
141            :param browse_record currency: currency for which we are rounding
142            :param float amount: the amount to round
143            :return: rounded float
144         """
145         return float_round(amount, precision_rounding=currency.rounding)
146
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!
152
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.
158
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.
165         """
166         return float_compare(amount1, amount2, precision_rounding=currency.rounding)
167
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.
171
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.
176
177            :param browse_record currency: currency for which we are rounding
178            :param float amount: amount to compare with currency's zero
179         """
180         return float_is_zero(amount, precision_rounding=currency.rounding)
181
182     def _get_conversion_rate(self, cr, uid, from_currency, to_currency, context=None):
183         if context is None:
184             context = {}
185         ctx = context.copy()
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)
188
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)
191
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
196             else:
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
202
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):
205         if not context:
206             context = {}
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):
215             if round:
216                 return self.round(cr, uid, to_currency, from_amount)
217             else:
218                 return from_amount
219         else:
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)
222             if round:
223                 return self.round(cr, uid, to_currency, from_amount * rate)
224             else:
225                 return from_amount * rate
226
227 res_currency()
228
229 class res_currency_rate_type(osv.osv):
230     _name = "res.currency.rate.type"
231     _description = "Currency Rate Type"
232     _columns = {
233         'name': fields.char('Name', size=64, required=True, translate=True),
234     }
235
236 res_currency_rate_type()
237
238 class res_currency_rate(osv.osv):
239     _name = "res.currency.rate"
240     _description = "Currency Rate"
241
242     _columns = {
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"),
247     }
248     _defaults = {
249         'name': lambda *a: time.strftime('%Y-%m-%d'),
250     }
251     _order = "name desc"
252
253 res_currency_rate()
254
255 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
256