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