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