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
17 from openerp import SUPERUSER_ID
19 _logger = logging.getLogger(__name__)
22 class AcquirerPaypal(osv.Model):
23 _inherit = 'payment.acquirer'
25 def _get_paypal_urls(self, cr, uid, env, context=None):
29 'paypal_form_url': 'https://www.paypal.com/cgi-bin/webscr',
30 'paypal_rest_url': 'https://api.paypal.com/v1/oauth2/token',
34 'paypal_form_url': 'https://www.sandbox.paypal.com/cgi-bin/webscr',
35 'paypal_rest_url': 'https://api.sandbox.paypal.com/v1/oauth2/token',
39 'paypal_email_account': fields.char('Paypal Email ID', required_if_provider='paypal'),
40 'paypal_seller_account': fields.char(
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'),
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'),
53 'paypal_use_ipn': True,
55 'fees_dom_fixed': 0.35,
57 'fees_int_fixed': 0.35,
59 'paypal_api_enabled': False,
62 def _migrate_paypal_account(self, cr, uid, context=None):
64 cr.execute('SELECT id, paypal_account FROM res_company')
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)
72 paypal_view = self.pool['ir.model.data'].get_object(cr, uid, 'payment_paypal', 'paypal_acquirer_button')
73 self.create(cr, uid, {
75 'paypal_email_account': company_paypal_account,
76 'view_template_id': paypal_view.id,
80 def paypal_compute_fees(self, cr, uid, id, amount, currency_id, country_id, context=None):
81 """ Compute paypal fees.
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
89 acquirer = self.browse(cr, uid, id, context=context)
90 if not acquirer.fees_active:
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
97 percentage = acquirer.fees_int_var
98 fixed = acquirer.fees_int_fixed
99 fees = (percentage / 100.0 * amount + fixed ) / (1 - percentage / 100.0)
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)
106 paypal_tx_values = dict(tx_values)
107 paypal_tx_values.update({
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),
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
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']
136 def _paypal_s2s_get_access_token(self, cr, uid, ids, context=None):
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
142 res = dict.fromkeys(ids, False)
143 parameters = werkzeug.url_encode({'grant_type': 'client_credentials'})
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)
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')
153 # add authorization header
154 base64string = base64.encodestring('%s:%s' % (
155 acquirer.paypal_api_username,
156 acquirer.paypal_api_password)
158 request.add_header("Authorization", "Basic %s" % base64string)
160 request = urllib2.urlopen(request)
161 result = request.read()
162 res[acquirer.id] = json.loads(result).get('access_token')
167 class TxPaypal(osv.Model):
168 _inherit = 'payment.transaction'
171 'paypal_txn_id': fields.char('Transaction ID'),
172 'paypal_txn_type': fields.char('Transaction type'),
175 # --------------------------------------------------
176 # FORM RELATED METHODS
177 # --------------------------------------------------
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)
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)
191 error_msg += '; no order found'
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)
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':
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')
205 if data.get('test_ipn'):
207 'Received a notification from Paypal using sandbox'
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))
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))
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))
229 return invalid_parameters
231 def _paypal_form_validate(self, cr, uid, tx, data, context=None):
232 status = data.get('payment_status')
234 'acquirer_reference': data.get('txn_id'),
235 'paypal_txn_type': data.get('payment_type'),
236 'partner_reference': data.get('payer_id')
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)
247 error = 'Received unrecognized status for Paypal payment %s: %s, set as error' % (tx.reference, status)
249 data.update(state='error', state_message=error)
250 return tx.write(data)
252 # --------------------------------------------------
253 # SERVER2SERVER RELATED METHODS
254 # --------------------------------------------------
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.
261 .. versionadded:: pre-v8 saas-3
264 Experimental code. You should not use it before OpenERP v8 official
267 done, res = False, None
268 while (not done and tries):
270 res = urllib2.urlopen(request)
272 except urllib2.HTTPError as e:
275 if tries and res and json.loads(res)['name'] == 'INTERNAL_SERVICE_ERROR':
276 _logger.warning('Failed contacting Paypal, retrying (%s remaining)' % tries)
280 # raise openerp.exceptions.
285 def _paypal_s2s_send(self, cr, uid, values, cc_values, context=None):
287 .. versionadded:: pre-v8 saas-3
290 Experimental code. You should not use it before OpenERP v8 official
293 tx_id = self.create(cr, uid, values, context=context)
294 tx = self.browse(cr, uid, tx_id, context=context)
297 'Content-Type': 'application/json',
298 'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
304 'total': '%.2f' % tx.amount,
305 'currency': tx.currency_id.name,
307 'description': tx.reference,
312 'payment_method': 'credit_card',
313 'funding_instruments': [{
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,
323 'line1': tx.partner_address,
324 'city': tx.partner_city,
325 'country_code': tx.partner_country_id.code,
326 'postal_code': tx.partner_zip,
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/',
338 'payment_method': 'paypal',
340 data = json.dumps(data)
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)
346 def _paypal_s2s_get_invalid_parameters(self, cr, uid, tx, data, context=None):
348 .. versionadded:: pre-v8 saas-3
351 Experimental code. You should not use it before OpenERP v8 official
354 invalid_parameters = []
355 return invalid_parameters
357 def _paypal_s2s_validate(self, cr, uid, tx, data, context=None):
359 .. versionadded:: pre-v8 saas-3
362 Experimental code. You should not use it before OpenERP v8 official
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))
371 'date_validate': values.get('udpate_time', fields.datetime.now()),
372 'paypal_txn_id': values['id'],
375 elif status in ['pending', 'expired']:
376 _logger.info('Received notification for Paypal s2s payment %s: set as pending' % (tx.reference))
379 # 'state_message': data.get('pending_reason', ''),
380 'paypal_txn_id': values['id'],
384 error = 'Received unrecognized status for Paypal s2s payment %s: %s, set as error' % (tx.reference, status)
388 # 'state_message': error,
389 'paypal_txn_id': values['id'],
393 def _paypal_s2s_get_tx_status(self, cr, uid, tx, context=None):
395 .. versionadded:: pre-v8 saas-3
398 Experimental code. You should not use it before OpenERP v8 official
401 # TDETODO: check tx.paypal_txn_id is set
403 'Content-Type': 'application/json',
404 'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
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)