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