a17f6ac83b5cdc27431c03323494d3135f38e212
[odoo/odoo.git] / addons / payment_paypal / models / paypal.py
1 # -*- coding: utf-'8' "-*-"
2
3 import base64
4 try:
5     import simplejson as json
6 except ImportError:
7     import json
8 import logging
9 import urlparse
10 import werkzeug.urls
11 import urllib2
12
13 from openerp.addons.payment.models.payment_acquirer import ValidationError
14 from openerp.addons.payment_paypal.controllers.main import PaypalController
15 from openerp.osv import osv, fields
16 from openerp.tools.float_utils import float_compare
17
18 _logger = logging.getLogger(__name__)
19
20
21 class AcquirerPaypal(osv.Model):
22     _inherit = 'payment.acquirer'
23
24     def _get_paypal_urls(self, cr, uid, env, context=None):
25         """ Paypal URLS """
26         if env == 'prod':
27             return {
28                 'paypal_form_url': 'https://www.paypal.com/cgi-bin/webscr',
29                 'paypal_rest_url': 'https://api.paypal.com/v1/oauth2/token',
30             }
31         else:
32             return {
33                 'paypal_form_url': 'https://www.sandbox.paypal.com/cgi-bin/webscr',
34                 'paypal_rest_url': 'https://api.sandbox.paypal.com/v1/oauth2/token',
35             }
36
37     _columns = {
38         'paypal_email_account': fields.char('Paypal Email ID', required_if_provider='paypal'),
39         'paypal_seller_account': fields.char(
40             'Paypal Merchant ID',
41             help='The Merchant ID is used to ensure communications coming from Paypal are valid and secured.'),
42         'paypal_use_ipn': fields.boolean('Use IPN', help='Paypal Instant Payment Notification'),
43         # Server 2 server
44         'paypal_api_enabled': fields.boolean('Use Rest API'),
45         'paypal_api_username': fields.char('Rest API Username'),
46         'paypal_api_password': fields.char('Rest API Password'),
47         'paypal_api_access_token': fields.char('Access Token'),
48         'paypal_api_access_token_validity': fields.datetime('Access Token Validity'),
49     }
50
51     _defaults = {
52         'paypal_use_ipn': True,
53         'fees_active': False,
54         'fees_dom_fixed': 0.35,
55         'fees_dom_var': 3.4,
56         'fees_int_fixed': 0.35,
57         'fees_int_var': 3.9,
58         'paypal_api_enabled': False,
59     }
60
61     def _migrate_paypal_account(self, cr, uid, context=None):
62         """ COMPLETE ME """
63         cr.execute('SELECT id, paypal_account FROM res_company')
64         res = cr.fetchall()
65         for (company_id, company_paypal_account) in res:
66             if company_paypal_account:
67                 company_paypal_ids = self.search(cr, uid, [('company_id', '=', company_id), ('name', '=', 'paypal')], limit=1, context=context)
68                 if company_paypal_ids:
69                     self.write(cr, uid, company_paypal_ids, {'paypal_email_account': company_paypal_account}, context=context)
70                 else:
71                     paypal_view = self.pool['ir.model.data'].get_object(cr, uid, 'payment_paypal', 'paypal_acquirer_button')
72                     self.create(cr, uid, {
73                         'name': 'paypal',
74                         'paypal_email_account': company_paypal_account,
75                         'view_template_id': paypal_view.id,
76                     }, context=context)
77         return True
78
79     def paypal_compute_fees(self, cr, uid, id, amount, currency_id, country_id, context=None):
80         """ Compute paypal fees.
81
82             :param float amount: the amount to pay
83             :param integer country_id: an ID of a res.country, or None. This is
84                                        the customer's country, to be compared to
85                                        the acquirer company country.
86             :return float fees: computed fees
87         """
88         acquirer = self.browse(cr, uid, id, context=context)
89         if not acquirer.fees_active:
90             return 0.0
91         country = self.pool['res.country'].browse(cr, uid, country_id, context=context)
92         if country and acquirer.company_id.country_id.id == country.id:
93             percentage = acquirer.fees_dom_var
94             fixed = acquirer.fees_dom_fixed
95         else:
96             percentage = acquirer.fees_int_var
97             fixed = acquirer.fees_int_fixed
98         fees = (percentage / 100.0 * amount + fixed ) / (1 - percentage / 100.0)
99         return fees
100
101     def paypal_form_generate_values(self, cr, uid, id, partner_values, tx_values, context=None):
102         base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
103         acquirer = self.browse(cr, uid, id, context=context)
104
105         paypal_tx_values = dict(tx_values)
106         paypal_tx_values.update({
107             'cmd': '_xclick',
108             'business': acquirer.paypal_email_account,
109             'item_name': tx_values['reference'],
110             'item_number': tx_values['reference'],
111             'amount': tx_values['amount'],
112             'currency_code': tx_values['currency'] and tx_values['currency'].name or '',
113             'address1': partner_values['address'],
114             'city': partner_values['city'],
115             'country': partner_values['country'] and partner_values['country'].name or '',
116             'state': partner_values['state'] and partner_values['state'].name or '',
117             'email': partner_values['email'],
118             'zip': partner_values['zip'],
119             'first_name': partner_values['first_name'],
120             'last_name': partner_values['last_name'],
121             'return': '%s' % urlparse.urljoin(base_url, PaypalController._return_url),
122             'notify_url': '%s' % urlparse.urljoin(base_url, PaypalController._notify_url),
123             'cancel_return': '%s' % urlparse.urljoin(base_url, PaypalController._cancel_url),
124         })
125         if acquirer.fees_active:
126             paypal_tx_values['handling'] = '%.2f' % paypal_tx_values.pop('fees', 0.0)
127         if paypal_tx_values.get('return_url'):
128             paypal_tx_values['custom'] = json.dumps({'return_url': '%s' % paypal_tx_values.pop('return_url')})
129         return partner_values, paypal_tx_values
130
131     def paypal_get_form_action_url(self, cr, uid, id, context=None):
132         acquirer = self.browse(cr, uid, id, context=context)
133         return self._get_paypal_urls(cr, uid, acquirer.env, context=context)['paypal_form_url']
134
135     def _paypal_s2s_get_access_token(self, cr, uid, ids, context=None):
136         """
137         Note: see # see http://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem
138         for explanation why we use Authorization header instead of urllib2
139         password manager
140         """
141         res = dict.fromkeys(ids, False)
142         parameters = werkzeug.url_encode({'grant_type': 'client_credentials'})
143
144         for acquirer in self.browse(cr, uid, ids, context=context):
145             tx_url = self._get_paypal_urls(cr, uid, acquirer.env)['paypal_rest_url']
146             request = urllib2.Request(tx_url, parameters)
147
148             # add other headers (https://developer.paypal.com/webapps/developer/docs/integration/direct/make-your-first-call/)
149             request.add_header('Accept', 'application/json')
150             request.add_header('Accept-Language', 'en_US')
151
152             # add authorization header
153             base64string = base64.encodestring('%s:%s' % (
154                 acquirer.paypal_api_username,
155                 acquirer.paypal_api_password)
156             ).replace('\n', '')
157             request.add_header("Authorization", "Basic %s" % base64string)
158
159             request = urllib2.urlopen(request)
160             result = request.read()
161             res[acquirer.id] = json.loads(result).get('access_token')
162             request.close()
163         return res
164
165
166 class TxPaypal(osv.Model):
167     _inherit = 'payment.transaction'
168
169     _columns = {
170         'paypal_txn_id': fields.char('Transaction ID'),
171         'paypal_txn_type': fields.char('Transaction type'),
172     }
173
174     # --------------------------------------------------
175     # FORM RELATED METHODS
176     # --------------------------------------------------
177
178     def _paypal_form_get_tx_from_data(self, cr, uid, data, context=None):
179         reference, txn_id = data.get('item_number'), data.get('txn_id')
180         if not reference or not txn_id:
181             error_msg = 'Paypal: received data with missing reference (%s) or txn_id (%s)' % (reference, txn_id)
182             _logger.error(error_msg)
183             raise ValidationError(error_msg)
184
185         # find tx -> @TDENOTE use txn_id ?
186         tx_ids = self.pool['payment.transaction'].search(cr, uid, [('reference', '=', reference)], context=context)
187         if not tx_ids or len(tx_ids) > 1:
188             error_msg = 'Paypal: received data for reference %s' % (reference)
189             if not tx_ids:
190                 error_msg += '; no order found'
191             else:
192                 error_msg += '; multiple order found'
193             _logger.error(error_msg)
194             raise ValidationError(error_msg)
195         return self.browse(cr, uid, tx_ids[0], context=context)
196
197     def _paypal_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
198         invalid_parameters = []
199         if data.get('notify_version')[0] != '3.4':
200             _logger.warning(
201                 'Received a notification from Paypal with version %s instead of 2.6. This could lead to issues when managing it.' %
202                 data.get('notify_version')
203             )
204         if data.get('test_ipn'):
205             _logger.warning(
206                 'Received a notification from Paypal using sandbox'
207             ),
208
209         # TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details
210         if tx.acquirer_reference and data.get('txn_id') != tx.acquirer_reference:
211             invalid_parameters.append(('txn_id', data.get('txn_id'), tx.acquirer_reference))
212         # check what is buyed
213         if float_compare(float(data.get('mc_gross', '0.0')), (tx.amount + tx.fees), 2) != 0:
214             invalid_parameters.append(('mc_gross', data.get('mc_gross'), '%.2f' % tx.amount))  # mc_gross is amount + fees
215         if data.get('mc_currency') != tx.currency_id.name:
216             invalid_parameters.append(('mc_currency', data.get('mc_currency'), tx.currency_id.name))
217         if 'handling_amount' in data and float_compare(float(data.get('handling_amount')), tx.fees, 2) != 0:
218             invalid_parameters.append(('handling_amount', data.get('handling_amount'), tx.fees))
219         # check buyer
220         if tx.partner_reference and data.get('payer_id') != tx.partner_reference:
221             invalid_parameters.append(('payer_id', data.get('payer_id'), tx.partner_reference))
222         # check seller
223         if data.get('receiver_email') != tx.acquirer_id.paypal_email_account:
224             invalid_parameters.append(('receiver_email', data.get('receiver_email'), tx.acquirer_id.paypal_email_account))
225         if data.get('receiver_id') and tx.acquirer_id.paypal_seller_account and data['receiver_id'] != tx.acquirer_id.paypal_seller_account:
226             invalid_parameters.append(('receiver_id', data.get('receiver_id'), tx.acquirer_id.paypal_seller_account))
227
228         return invalid_parameters
229
230     def _paypal_form_validate(self, cr, uid, tx, data, context=None):
231         status = data.get('payment_status')
232         data = {
233             'acquirer_reference': data.get('txn_id'),
234             'paypal_txn_type': data.get('payment_type'),
235             'partner_reference': data.get('payer_id')
236         }
237         if status in ['Completed', 'Processed']:
238             _logger.info('Validated Paypal payment for tx %s: set as done' % (tx.reference))
239             data.update(state='done', date_validate=data.get('payment_date', fields.datetime.now()))
240             return tx.write(data)
241         elif status in ['Pending', 'Expired']:
242             _logger.info('Received notification for Paypal payment %s: set as pending' % (tx.reference))
243             data.update(state='pending', state_message=data.get('pending_reason', ''))
244             return tx.write(data)
245         else:
246             error = 'Received unrecognized status for Paypal payment %s: %s, set as error' % (tx.reference, status)
247             _logger.info(error)
248             data.update(state='error', state_message=error)
249             return tx.write(data)
250
251     # --------------------------------------------------
252     # SERVER2SERVER RELATED METHODS
253     # --------------------------------------------------
254
255     def _paypal_try_url(self, request, tries=3, context=None):
256         """ Try to contact Paypal. Due to some issues, internal service errors
257         seem to be quite frequent. Several tries are done before considering
258         the communication as failed.
259
260          .. versionadded:: pre-v8 saas-3
261          .. warning::
262
263             Experimental code. You should not use it before OpenERP v8 official
264             release.
265         """
266         done, res = False, None
267         while (not done and tries):
268             try:
269                 res = urllib2.urlopen(request)
270                 done = True
271             except urllib2.HTTPError as e:
272                 res = e.read()
273                 e.close()
274                 if tries and res and json.loads(res)['name'] == 'INTERNAL_SERVICE_ERROR':
275                     _logger.warning('Failed contacting Paypal, retrying (%s remaining)' % tries)
276             tries = tries - 1
277         if not res:
278             pass
279             # raise openerp.exceptions.
280         result = res.read()
281         res.close()
282         return result
283
284     def _paypal_s2s_send(self, cr, uid, values, cc_values, context=None):
285         """
286          .. versionadded:: pre-v8 saas-3
287          .. warning::
288
289             Experimental code. You should not use it before OpenERP v8 official
290             release.
291         """
292         tx_id = self.create(cr, uid, values, context=context)
293         tx = self.browse(cr, uid, tx_id, context=context)
294
295         headers = {
296             'Content-Type': 'application/json',
297             'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
298         }
299         data = {
300             'intent': 'sale',
301             'transactions': [{
302                 'amount': {
303                     'total': '%.2f' % tx.amount,
304                     'currency': tx.currency_id.name,
305                 },
306                 'description': tx.reference,
307             }]
308         }
309         if cc_values:
310             data['payer'] = {
311                 'payment_method': 'credit_card',
312                 'funding_instruments': [{
313                     'credit_card': {
314                         'number': cc_values['number'],
315                         'type': cc_values['brand'],
316                         'expire_month': cc_values['expiry_mm'],
317                         'expire_year': cc_values['expiry_yy'],
318                         'cvv2': cc_values['cvc'],
319                         'first_name': tx.partner_name,
320                         'last_name': tx.partner_name,
321                         'billing_address': {
322                             'line1': tx.partner_address,
323                             'city': tx.partner_city,
324                             'country_code': tx.partner_country_id.code,
325                             'postal_code': tx.partner_zip,
326                         }
327                     }
328                 }]
329             }
330         else:
331             # TODO: complete redirect URLs
332             data['redirect_urls'] = {
333                 # 'return_url': 'http://example.com/your_redirect_url/',
334                 # 'cancel_url': 'http://example.com/your_cancel_url/',
335             },
336             data['payer'] = {
337                 'payment_method': 'paypal',
338             }
339         data = json.dumps(data)
340
341         request = urllib2.Request('https://api.sandbox.paypal.com/v1/payments/payment', data, headers)
342         result = self._paypal_try_url(request, tries=3, context=context)
343         return (tx_id, result)
344
345     def _paypal_s2s_get_invalid_parameters(self, cr, uid, tx, data, context=None):
346         """
347          .. versionadded:: pre-v8 saas-3
348          .. warning::
349
350             Experimental code. You should not use it before OpenERP v8 official
351             release.
352         """
353         invalid_parameters = []
354         return invalid_parameters
355
356     def _paypal_s2s_validate(self, cr, uid, tx, data, context=None):
357         """
358          .. versionadded:: pre-v8 saas-3
359          .. warning::
360
361             Experimental code. You should not use it before OpenERP v8 official
362             release.
363         """
364         values = json.loads(data)
365         status = values.get('state')
366         if status in ['approved']:
367             _logger.info('Validated Paypal s2s payment for tx %s: set as done' % (tx.reference))
368             tx.write({
369                 'state': 'done',
370                 'date_validate': values.get('udpate_time', fields.datetime.now()),
371                 'paypal_txn_id': values['id'],
372             })
373             return True
374         elif status in ['pending', 'expired']:
375             _logger.info('Received notification for Paypal s2s payment %s: set as pending' % (tx.reference))
376             tx.write({
377                 'state': 'pending',
378                 # 'state_message': data.get('pending_reason', ''),
379                 'paypal_txn_id': values['id'],
380             })
381             return True
382         else:
383             error = 'Received unrecognized status for Paypal s2s payment %s: %s, set as error' % (tx.reference, status)
384             _logger.info(error)
385             tx.write({
386                 'state': 'error',
387                 # 'state_message': error,
388                 'paypal_txn_id': values['id'],
389             })
390             return False
391
392     def _paypal_s2s_get_tx_status(self, cr, uid, tx, context=None):
393         """
394          .. versionadded:: pre-v8 saas-3
395          .. warning::
396
397             Experimental code. You should not use it before OpenERP v8 official
398             release.
399         """
400         # TDETODO: check tx.paypal_txn_id is set
401         headers = {
402             'Content-Type': 'application/json',
403             'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
404         }
405         url = 'https://api.sandbox.paypal.com/v1/payments/payment/%s' % (tx.paypal_txn_id)
406         request = urllib2.Request(url, headers=headers)
407         data = self._paypal_try_url(request, tries=3, context=context)
408         return self.s2s_feedback(cr, uid, tx.id, data, context=context)