a5e31e9571cc432588283b99d5a06f373e0d069a
[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 Seller ID',
41             help='The Seller 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             fees = amount * (1 + acquirer.fees_dom_var / 100.0) + acquirer.fees_dom_fixed - amount
94         else:
95             fees = amount * (1 + acquirer.fees_int_var / 100.0) + acquirer.fees_int_fixed - amount
96         return fees
97
98     def paypal_form_generate_values(self, cr, uid, id, partner_values, tx_values, context=None):
99         base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
100         acquirer = self.browse(cr, uid, id, context=context)
101
102         paypal_tx_values = dict(tx_values)
103         paypal_tx_values.update({
104             'cmd': '_xclick',
105             'business': acquirer.paypal_email_account,
106             'item_name': tx_values['reference'],
107             'item_number': tx_values['reference'],
108             'amount': tx_values['amount'],
109             'currency_code': tx_values['currency'] and tx_values['currency'].name or '',
110             'address1': partner_values['address'],
111             'city': partner_values['city'],
112             'country': partner_values['country'] and partner_values['country'].name or '',
113             'email': partner_values['email'],
114             'zip': partner_values['zip'],
115             'first_name': partner_values['first_name'],
116             'last_name': partner_values['last_name'],
117             'return': '%s' % urlparse.urljoin(base_url, PaypalController._return_url),
118             'notify_url': '%s' % urlparse.urljoin(base_url, PaypalController._notify_url),
119             'cancel_return': '%s' % urlparse.urljoin(base_url, PaypalController._cancel_url),
120         })
121         if acquirer.fees_active:
122             paypal_tx_values['handling'] = '%.2f' % paypal_tx_values.pop('fees', 0.0)
123         if paypal_tx_values.get('return_url'):
124             paypal_tx_values['custom'] = json.dumps({'return_url': '%s' % paypal_tx_values.pop('return_url')})
125         return partner_values, paypal_tx_values
126
127     def paypal_get_form_action_url(self, cr, uid, id, context=None):
128         acquirer = self.browse(cr, uid, id, context=context)
129         return self._get_paypal_urls(cr, uid, acquirer.env, context=context)['paypal_form_url']
130
131     def _paypal_s2s_get_access_token(self, cr, uid, ids, context=None):
132         """
133         Note: see # see http://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem
134         for explanation why we use Authorization header instead of urllib2
135         password manager
136         """
137         res = dict.fromkeys(ids, False)
138         parameters = werkzeug.url_encode({'grant_type': 'client_credentials'})
139
140         for acquirer in self.browse(cr, uid, ids, context=context):
141             tx_url = self._get_paypal_urls(cr, uid, acquirer.env)['paypal_rest_url']
142             request = urllib2.Request(tx_url, parameters)
143
144             # add other headers (https://developer.paypal.com/webapps/developer/docs/integration/direct/make-your-first-call/)
145             request.add_header('Accept', 'application/json')
146             request.add_header('Accept-Language', 'en_US')
147
148             # add authorization header
149             base64string = base64.encodestring('%s:%s' % (
150                 acquirer.paypal_api_username,
151                 acquirer.paypal_api_password)
152             ).replace('\n', '')
153             request.add_header("Authorization", "Basic %s" % base64string)
154
155             request = urllib2.urlopen(request)
156             result = request.read()
157             res[acquirer.id] = json.loads(result).get('access_token')
158             request.close()
159         return res
160
161
162 class TxPaypal(osv.Model):
163     _inherit = 'payment.transaction'
164
165     _columns = {
166         'paypal_txn_id': fields.char('Transaction ID'),
167         'paypal_txn_type': fields.char('Transaction type'),
168     }
169
170     # --------------------------------------------------
171     # FORM RELATED METHODS
172     # --------------------------------------------------
173
174     def _paypal_form_get_tx_from_data(self, cr, uid, data, context=None):
175         reference, txn_id = data.get('item_number'), data.get('txn_id')
176         if not reference or not txn_id:
177             error_msg = 'Paypal: received data with missing reference (%s) or txn_id (%s)' % (reference, txn_id)
178             _logger.error(error_msg)
179             raise ValidationError(error_msg)
180
181         # find tx -> @TDENOTE use txn_id ?
182         tx_ids = self.pool['payment.transaction'].search(cr, uid, [('reference', '=', reference)], context=context)
183         if not tx_ids or len(tx_ids) > 1:
184             error_msg = 'Paypal: received data for reference %s' % (reference)
185             if not tx_ids:
186                 error_msg += '; no order found'
187             else:
188                 error_msg += '; multiple order found'
189             _logger.error(error_msg)
190             raise ValidationError(error_msg)
191         return self.browse(cr, uid, tx_ids[0], context=context)
192
193     def _paypal_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
194         invalid_parameters = []
195         if data.get('notify_version')[0] != '3.4':
196             _logger.warning(
197                 'Received a notification from Paypal with version %s instead of 2.6. This could lead to issues when managing it.' %
198                 data.get('notify_version')
199             )
200         if data.get('test_ipn'):
201             _logger.warning(
202                 'Received a notification from Paypal using sandbox'
203             ),
204
205         # TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details
206         if tx.acquirer_reference and data.get('txn_id') != tx.acquirer_reference:
207             invalid_parameters.append(('txn_id', data.get('txn_id'), tx.acquirer_reference))
208         # check what is buyed
209         if float_compare(float(data.get('mc_gross', '0.0')), (tx.amount + tx.fees), 2) != 0:
210             invalid_parameters.append(('mc_gross', data.get('mc_gross'), '%.2f' % tx.amount))  # mc_gross is amount + fees
211         if data.get('mc_currency') != tx.currency_id.name:
212             invalid_parameters.append(('mc_currency', data.get('mc_currency'), tx.currency_id.name))
213         if 'handling_amount' in data and float_compare(float(data.get('handling_amount')), tx.fees, 2) != 0:
214             invalid_parameters.append(('handling_amount', data.get('handling_amount'), tx.fees))
215         # check buyer
216         if tx.partner_reference and data.get('payer_id') != tx.partner_reference:
217             invalid_parameters.append(('payer_id', data.get('payer_id'), tx.partner_reference))
218         # check seller
219         if data.get('receiver_email') != tx.acquirer_id.paypal_email_account:
220             invalid_parameters.append(('receiver_email', data.get('receiver_email'), tx.acquirer_id.paypal_email_account))
221         if tx.acquirer_id.paypal_seller_account and data.get('receiver_id') != tx.acquirer_id.paypal_seller_account:
222             invalid_parameters.append(('receiver_id', data.get('receiver_id'), tx.acquirer_id.paypal_seller_account))
223
224         return invalid_parameters
225
226     def _paypal_form_validate(self, cr, uid, tx, data, context=None):
227         status = data.get('payment_status')
228         data = {
229             'acquirer_reference': data.get('txn_id'),
230             'paypal_txn_type': data.get('payment_type'),
231             'partner_reference': data.get('payer_id')
232         }
233         if status in ['Completed', 'Processed']:
234             _logger.info('Validated Paypal payment for tx %s: set as done' % (tx.reference))
235             data.update(state='done', date_validate=data.get('payment_date', fields.datetime.now()))
236             return tx.write(data)
237         elif status in ['Pending', 'Expired']:
238             _logger.info('Received notification for Paypal payment %s: set as pending' % (tx.reference))
239             data.update(state='pending', state_message=data.get('pending_reason', ''))
240             return tx.write(data)
241         else:
242             error = 'Received unrecognized status for Paypal payment %s: %s, set as error' % (tx.reference, status)
243             _logger.info(error)
244             data.update(state='error', state_message=error)
245             return tx.write(data)
246
247     # --------------------------------------------------
248     # SERVER2SERVER RELATED METHODS
249     # --------------------------------------------------
250
251     def _paypal_try_url(self, request, tries=3, context=None):
252         """ Try to contact Paypal. Due to some issues, internal service errors
253         seem to be quite frequent. Several tries are done before considering
254         the communication as failed.
255
256          .. versionadded:: pre-v8 saas-3
257          .. warning::
258
259             Experimental code. You should not use it before OpenERP v8 official
260             release.
261         """
262         done, res = False, None
263         while (not done and tries):
264             try:
265                 res = urllib2.urlopen(request)
266                 done = True
267             except urllib2.HTTPError as e:
268                 res = e.read()
269                 e.close()
270                 if tries and res and json.loads(res)['name'] == 'INTERNAL_SERVICE_ERROR':
271                     _logger.warning('Failed contacting Paypal, retrying (%s remaining)' % tries)
272             tries = tries - 1
273         if not res:
274             pass
275             # raise openerp.exceptions.
276         result = res.read()
277         res.close()
278         return result
279
280     def _paypal_s2s_send(self, cr, uid, values, cc_values, context=None):
281         """
282          .. versionadded:: pre-v8 saas-3
283          .. warning::
284
285             Experimental code. You should not use it before OpenERP v8 official
286             release.
287         """
288         tx_id = self.create(cr, uid, values, context=context)
289         tx = self.browse(cr, uid, tx_id, context=context)
290
291         headers = {
292             'Content-Type': 'application/json',
293             'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
294         }
295         data = {
296             'intent': 'sale',
297             'transactions': [{
298                 'amount': {
299                     'total': '%.2f' % tx.amount,
300                     'currency': tx.currency_id.name,
301                 },
302                 'description': tx.reference,
303             }]
304         }
305         if cc_values:
306             data['payer'] = {
307                 'payment_method': 'credit_card',
308                 'funding_instruments': [{
309                     'credit_card': {
310                         'number': cc_values['number'],
311                         'type': cc_values['brand'],
312                         'expire_month': cc_values['expiry_mm'],
313                         'expire_year': cc_values['expiry_yy'],
314                         'cvv2': cc_values['cvc'],
315                         'first_name': tx.partner_name,
316                         'last_name': tx.partner_name,
317                         'billing_address': {
318                             'line1': tx.partner_address,
319                             'city': tx.partner_city,
320                             'country_code': tx.partner_country_id.code,
321                             'postal_code': tx.partner_zip,
322                         }
323                     }
324                 }]
325             }
326         else:
327             # TODO: complete redirect URLs
328             data['redirect_urls'] = {
329                 # 'return_url': 'http://example.com/your_redirect_url/',
330                 # 'cancel_url': 'http://example.com/your_cancel_url/',
331             },
332             data['payer'] = {
333                 'payment_method': 'paypal',
334             }
335         data = json.dumps(data)
336
337         request = urllib2.Request('https://api.sandbox.paypal.com/v1/payments/payment', data, headers)
338         result = self._paypal_try_url(request, tries=3, context=context)
339         return (tx_id, result)
340
341     def _paypal_s2s_get_invalid_parameters(self, cr, uid, tx, data, context=None):
342         """
343          .. versionadded:: pre-v8 saas-3
344          .. warning::
345
346             Experimental code. You should not use it before OpenERP v8 official
347             release.
348         """
349         invalid_parameters = []
350         return invalid_parameters
351
352     def _paypal_s2s_validate(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         values = json.loads(data)
361         status = values.get('state')
362         if status in ['approved']:
363             _logger.info('Validated Paypal s2s payment for tx %s: set as done' % (tx.reference))
364             tx.write({
365                 'state': 'done',
366                 'date_validate': values.get('udpate_time', fields.datetime.now()),
367                 'paypal_txn_id': values['id'],
368             })
369             return True
370         elif status in ['pending', 'expired']:
371             _logger.info('Received notification for Paypal s2s payment %s: set as pending' % (tx.reference))
372             tx.write({
373                 'state': 'pending',
374                 # 'state_message': data.get('pending_reason', ''),
375                 'paypal_txn_id': values['id'],
376             })
377             return True
378         else:
379             error = 'Received unrecognized status for Paypal s2s payment %s: %s, set as error' % (tx.reference, status)
380             _logger.info(error)
381             tx.write({
382                 'state': 'error',
383                 # 'state_message': error,
384                 'paypal_txn_id': values['id'],
385             })
386             return False
387
388     def _paypal_s2s_get_tx_status(self, cr, uid, tx, context=None):
389         """
390          .. versionadded:: pre-v8 saas-3
391          .. warning::
392
393             Experimental code. You should not use it before OpenERP v8 official
394             release.
395         """
396         # TDETODO: check tx.paypal_txn_id is set
397         headers = {
398             'Content-Type': 'application/json',
399             'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
400         }
401         url = 'https://api.sandbox.paypal.com/v1/payments/payment/%s' % (tx.paypal_txn_id)
402         request = urllib2.Request(url, headers=headers)
403         data = self._paypal_try_url(request, tries=3, context=context)
404         return self.s2s_feedback(cr, uid, tx.id, data, context=context)