1 # -*- coding: utf-'8' "-*-"
5 import simplejson as json
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
18 _logger = logging.getLogger(__name__)
21 class AcquirerPaypal(osv.Model):
22 _inherit = 'payment.acquirer'
24 def _get_paypal_urls(self, cr, uid, env, context=None):
28 'paypal_form_url': 'https://www.paypal.com/cgi-bin/webscr',
29 'paypal_rest_url': 'https://api.paypal.com/v1/oauth2/token',
33 'paypal_form_url': 'https://www.sandbox.paypal.com/cgi-bin/webscr',
34 'paypal_rest_url': 'https://api.sandbox.paypal.com/v1/oauth2/token',
38 'paypal_email_account': fields.char('Paypal Email ID', required_if_provider='paypal'),
39 'paypal_seller_account': fields.char(
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'),
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'),
52 'paypal_use_ipn': True,
54 'fees_dom_fixed': 0.35,
56 'fees_int_fixed': 0.35,
58 'paypal_api_enabled': False,
61 def _migrate_paypal_account(self, cr, uid, context=None):
63 cr.execute('SELECT id, paypal_account FROM res_company')
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)
71 paypal_view = self.pool['ir.model.data'].get_object(cr, uid, 'payment_paypal', 'paypal_acquirer_button')
72 self.create(cr, uid, {
74 'paypal_email_account': company_paypal_account,
75 'view_template_id': paypal_view.id,
79 def paypal_compute_fees(self, cr, uid, id, amount, currency_id, country_id, context=None):
80 """ Compute paypal fees.
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
88 acquirer = self.browse(cr, uid, id, context=context)
89 if not acquirer.fees_active:
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
95 fees = amount * (1 + acquirer.fees_int_var / 100.0) + acquirer.fees_int_fixed - amount
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)
102 paypal_tx_values = dict(tx_values)
103 paypal_tx_values.update({
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),
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
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']
131 def _paypal_s2s_get_access_token(self, cr, uid, ids, context=None):
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
137 res = dict.fromkeys(ids, False)
138 parameters = werkzeug.url_encode({'grant_type': 'client_credentials'})
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)
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')
148 # add authorization header
149 base64string = base64.encodestring('%s:%s' % (
150 acquirer.paypal_api_username,
151 acquirer.paypal_api_password)
153 request.add_header("Authorization", "Basic %s" % base64string)
155 request = urllib2.urlopen(request)
156 result = request.read()
157 res[acquirer.id] = json.loads(result).get('access_token')
162 class TxPaypal(osv.Model):
163 _inherit = 'payment.transaction'
166 'paypal_txn_id': fields.char('Transaction ID'),
167 'paypal_txn_type': fields.char('Transaction type'),
170 # --------------------------------------------------
171 # FORM RELATED METHODS
172 # --------------------------------------------------
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)
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)
186 error_msg += '; no order found'
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)
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':
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')
200 if data.get('test_ipn'):
202 'Received a notification from Paypal using sandbox'
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))
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))
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))
224 return invalid_parameters
226 def _paypal_form_validate(self, cr, uid, tx, data, context=None):
227 status = data.get('payment_status')
229 'acquirer_reference': data.get('txn_id'),
230 'paypal_txn_type': data.get('payment_type'),
231 'partner_reference': data.get('payer_id')
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)
242 error = 'Received unrecognized status for Paypal payment %s: %s, set as error' % (tx.reference, status)
244 data.update(state='error', state_message=error)
245 return tx.write(data)
247 # --------------------------------------------------
248 # SERVER2SERVER RELATED METHODS
249 # --------------------------------------------------
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.
256 .. versionadded:: pre-v8 saas-3
259 Experimental code. You should not use it before OpenERP v8 official
262 done, res = False, None
263 while (not done and tries):
265 res = urllib2.urlopen(request)
267 except urllib2.HTTPError as e:
270 if tries and res and json.loads(res)['name'] == 'INTERNAL_SERVICE_ERROR':
271 _logger.warning('Failed contacting Paypal, retrying (%s remaining)' % tries)
275 # raise openerp.exceptions.
280 def _paypal_s2s_send(self, cr, uid, values, cc_values, context=None):
282 .. versionadded:: pre-v8 saas-3
285 Experimental code. You should not use it before OpenERP v8 official
288 tx_id = self.create(cr, uid, values, context=context)
289 tx = self.browse(cr, uid, tx_id, context=context)
292 'Content-Type': 'application/json',
293 'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
299 'total': '%.2f' % tx.amount,
300 'currency': tx.currency_id.name,
302 'description': tx.reference,
307 'payment_method': 'credit_card',
308 'funding_instruments': [{
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,
318 'line1': tx.partner_address,
319 'city': tx.partner_city,
320 'country_code': tx.partner_country_id.code,
321 'postal_code': tx.partner_zip,
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/',
333 'payment_method': 'paypal',
335 data = json.dumps(data)
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)
341 def _paypal_s2s_get_invalid_parameters(self, cr, uid, tx, data, context=None):
343 .. versionadded:: pre-v8 saas-3
346 Experimental code. You should not use it before OpenERP v8 official
349 invalid_parameters = []
350 return invalid_parameters
352 def _paypal_s2s_validate(self, cr, uid, tx, data, context=None):
354 .. versionadded:: pre-v8 saas-3
357 Experimental code. You should not use it before OpenERP v8 official
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))
366 'date_validate': values.get('udpate_time', fields.datetime.now()),
367 'paypal_txn_id': values['id'],
370 elif status in ['pending', 'expired']:
371 _logger.info('Received notification for Paypal s2s payment %s: set as pending' % (tx.reference))
374 # 'state_message': data.get('pending_reason', ''),
375 'paypal_txn_id': values['id'],
379 error = 'Received unrecognized status for Paypal s2s payment %s: %s, set as error' % (tx.reference, status)
383 # 'state_message': error,
384 'paypal_txn_id': values['id'],
388 def _paypal_s2s_get_tx_status(self, cr, uid, tx, context=None):
390 .. versionadded:: pre-v8 saas-3
393 Experimental code. You should not use it before OpenERP v8 official
396 # TDETODO: check tx.paypal_txn_id is set
398 'Content-Type': 'application/json',
399 'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
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)