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 Merchant 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 percentage = acquirer.fees_dom_var
94 fixed = acquirer.fees_dom_fixed
96 percentage = acquirer.fees_int_var
97 fixed = acquirer.fees_int_fixed
98 fees = (percentage / 100.0 * amount + fixed ) / (1 - percentage / 100.0)
101 def paypal_form_generate_values(self, cr, uid, id, partner_values, tx_values, context=None):
102 base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
103 acquirer = self.browse(cr, uid, id, context=context)
105 paypal_tx_values = dict(tx_values)
106 paypal_tx_values.update({
108 'business': acquirer.paypal_email_account,
109 'item_name': tx_values['reference'],
110 'item_number': tx_values['reference'],
111 'amount': tx_values['amount'],
112 'currency_code': tx_values['currency'] and tx_values['currency'].name or '',
113 'address1': partner_values['address'],
114 'city': partner_values['city'],
115 'country': partner_values['country'] and partner_values['country'].name or '',
116 'state': partner_values['state'] and partner_values['state'].name or '',
117 'email': partner_values['email'],
118 'zip': partner_values['zip'],
119 'first_name': partner_values['first_name'],
120 'last_name': partner_values['last_name'],
121 'return': '%s' % urlparse.urljoin(base_url, PaypalController._return_url),
122 'notify_url': '%s' % urlparse.urljoin(base_url, PaypalController._notify_url),
123 'cancel_return': '%s' % urlparse.urljoin(base_url, PaypalController._cancel_url),
125 if acquirer.fees_active:
126 paypal_tx_values['handling'] = '%.2f' % paypal_tx_values.pop('fees', 0.0)
127 if paypal_tx_values.get('return_url'):
128 paypal_tx_values['custom'] = json.dumps({'return_url': '%s' % paypal_tx_values.pop('return_url')})
129 return partner_values, paypal_tx_values
131 def paypal_get_form_action_url(self, cr, uid, id, context=None):
132 acquirer = self.browse(cr, uid, id, context=context)
133 return self._get_paypal_urls(cr, uid, acquirer.env, context=context)['paypal_form_url']
135 def _paypal_s2s_get_access_token(self, cr, uid, ids, context=None):
137 Note: see # see http://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem
138 for explanation why we use Authorization header instead of urllib2
141 res = dict.fromkeys(ids, False)
142 parameters = werkzeug.url_encode({'grant_type': 'client_credentials'})
144 for acquirer in self.browse(cr, uid, ids, context=context):
145 tx_url = self._get_paypal_urls(cr, uid, acquirer.env)['paypal_rest_url']
146 request = urllib2.Request(tx_url, parameters)
148 # add other headers (https://developer.paypal.com/webapps/developer/docs/integration/direct/make-your-first-call/)
149 request.add_header('Accept', 'application/json')
150 request.add_header('Accept-Language', 'en_US')
152 # add authorization header
153 base64string = base64.encodestring('%s:%s' % (
154 acquirer.paypal_api_username,
155 acquirer.paypal_api_password)
157 request.add_header("Authorization", "Basic %s" % base64string)
159 request = urllib2.urlopen(request)
160 result = request.read()
161 res[acquirer.id] = json.loads(result).get('access_token')
166 class TxPaypal(osv.Model):
167 _inherit = 'payment.transaction'
170 'paypal_txn_id': fields.char('Transaction ID'),
171 'paypal_txn_type': fields.char('Transaction type'),
174 # --------------------------------------------------
175 # FORM RELATED METHODS
176 # --------------------------------------------------
178 def _paypal_form_get_tx_from_data(self, cr, uid, data, context=None):
179 reference, txn_id = data.get('item_number'), data.get('txn_id')
180 if not reference or not txn_id:
181 error_msg = 'Paypal: received data with missing reference (%s) or txn_id (%s)' % (reference, txn_id)
182 _logger.error(error_msg)
183 raise ValidationError(error_msg)
185 # find tx -> @TDENOTE use txn_id ?
186 tx_ids = self.pool['payment.transaction'].search(cr, uid, [('reference', '=', reference)], context=context)
187 if not tx_ids or len(tx_ids) > 1:
188 error_msg = 'Paypal: received data for reference %s' % (reference)
190 error_msg += '; no order found'
192 error_msg += '; multiple order found'
193 _logger.error(error_msg)
194 raise ValidationError(error_msg)
195 return self.browse(cr, uid, tx_ids[0], context=context)
197 def _paypal_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
198 invalid_parameters = []
199 if data.get('notify_version')[0] != '3.4':
201 'Received a notification from Paypal with version %s instead of 2.6. This could lead to issues when managing it.' %
202 data.get('notify_version')
204 if data.get('test_ipn'):
206 'Received a notification from Paypal using sandbox'
209 # TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details
210 if tx.acquirer_reference and data.get('txn_id') != tx.acquirer_reference:
211 invalid_parameters.append(('txn_id', data.get('txn_id'), tx.acquirer_reference))
212 # check what is buyed
213 if float_compare(float(data.get('mc_gross', '0.0')), (tx.amount + tx.fees), 2) != 0:
214 invalid_parameters.append(('mc_gross', data.get('mc_gross'), '%.2f' % tx.amount)) # mc_gross is amount + fees
215 if data.get('mc_currency') != tx.currency_id.name:
216 invalid_parameters.append(('mc_currency', data.get('mc_currency'), tx.currency_id.name))
217 if 'handling_amount' in data and float_compare(float(data.get('handling_amount')), tx.fees, 2) != 0:
218 invalid_parameters.append(('handling_amount', data.get('handling_amount'), tx.fees))
220 if tx.partner_reference and data.get('payer_id') != tx.partner_reference:
221 invalid_parameters.append(('payer_id', data.get('payer_id'), tx.partner_reference))
223 if data.get('receiver_email') != tx.acquirer_id.paypal_email_account:
224 invalid_parameters.append(('receiver_email', data.get('receiver_email'), tx.acquirer_id.paypal_email_account))
225 if data.get('receiver_id') and tx.acquirer_id.paypal_seller_account and data['receiver_id'] != tx.acquirer_id.paypal_seller_account:
226 invalid_parameters.append(('receiver_id', data.get('receiver_id'), tx.acquirer_id.paypal_seller_account))
228 return invalid_parameters
230 def _paypal_form_validate(self, cr, uid, tx, data, context=None):
231 status = data.get('payment_status')
233 'acquirer_reference': data.get('txn_id'),
234 'paypal_txn_type': data.get('payment_type'),
235 'partner_reference': data.get('payer_id')
237 if status in ['Completed', 'Processed']:
238 _logger.info('Validated Paypal payment for tx %s: set as done' % (tx.reference))
239 data.update(state='done', date_validate=data.get('payment_date', fields.datetime.now()))
240 return tx.write(data)
241 elif status in ['Pending', 'Expired']:
242 _logger.info('Received notification for Paypal payment %s: set as pending' % (tx.reference))
243 data.update(state='pending', state_message=data.get('pending_reason', ''))
244 return tx.write(data)
246 error = 'Received unrecognized status for Paypal payment %s: %s, set as error' % (tx.reference, status)
248 data.update(state='error', state_message=error)
249 return tx.write(data)
251 # --------------------------------------------------
252 # SERVER2SERVER RELATED METHODS
253 # --------------------------------------------------
255 def _paypal_try_url(self, request, tries=3, context=None):
256 """ Try to contact Paypal. Due to some issues, internal service errors
257 seem to be quite frequent. Several tries are done before considering
258 the communication as failed.
260 .. versionadded:: pre-v8 saas-3
263 Experimental code. You should not use it before OpenERP v8 official
266 done, res = False, None
267 while (not done and tries):
269 res = urllib2.urlopen(request)
271 except urllib2.HTTPError as e:
274 if tries and res and json.loads(res)['name'] == 'INTERNAL_SERVICE_ERROR':
275 _logger.warning('Failed contacting Paypal, retrying (%s remaining)' % tries)
279 # raise openerp.exceptions.
284 def _paypal_s2s_send(self, cr, uid, values, cc_values, context=None):
286 .. versionadded:: pre-v8 saas-3
289 Experimental code. You should not use it before OpenERP v8 official
292 tx_id = self.create(cr, uid, values, context=context)
293 tx = self.browse(cr, uid, tx_id, context=context)
296 'Content-Type': 'application/json',
297 'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
303 'total': '%.2f' % tx.amount,
304 'currency': tx.currency_id.name,
306 'description': tx.reference,
311 'payment_method': 'credit_card',
312 'funding_instruments': [{
314 'number': cc_values['number'],
315 'type': cc_values['brand'],
316 'expire_month': cc_values['expiry_mm'],
317 'expire_year': cc_values['expiry_yy'],
318 'cvv2': cc_values['cvc'],
319 'first_name': tx.partner_name,
320 'last_name': tx.partner_name,
322 'line1': tx.partner_address,
323 'city': tx.partner_city,
324 'country_code': tx.partner_country_id.code,
325 'postal_code': tx.partner_zip,
331 # TODO: complete redirect URLs
332 data['redirect_urls'] = {
333 # 'return_url': 'http://example.com/your_redirect_url/',
334 # 'cancel_url': 'http://example.com/your_cancel_url/',
337 'payment_method': 'paypal',
339 data = json.dumps(data)
341 request = urllib2.Request('https://api.sandbox.paypal.com/v1/payments/payment', data, headers)
342 result = self._paypal_try_url(request, tries=3, context=context)
343 return (tx_id, result)
345 def _paypal_s2s_get_invalid_parameters(self, cr, uid, tx, data, context=None):
347 .. versionadded:: pre-v8 saas-3
350 Experimental code. You should not use it before OpenERP v8 official
353 invalid_parameters = []
354 return invalid_parameters
356 def _paypal_s2s_validate(self, cr, uid, tx, data, context=None):
358 .. versionadded:: pre-v8 saas-3
361 Experimental code. You should not use it before OpenERP v8 official
364 values = json.loads(data)
365 status = values.get('state')
366 if status in ['approved']:
367 _logger.info('Validated Paypal s2s payment for tx %s: set as done' % (tx.reference))
370 'date_validate': values.get('udpate_time', fields.datetime.now()),
371 'paypal_txn_id': values['id'],
374 elif status in ['pending', 'expired']:
375 _logger.info('Received notification for Paypal s2s payment %s: set as pending' % (tx.reference))
378 # 'state_message': data.get('pending_reason', ''),
379 'paypal_txn_id': values['id'],
383 error = 'Received unrecognized status for Paypal s2s payment %s: %s, set as error' % (tx.reference, status)
387 # 'state_message': error,
388 'paypal_txn_id': values['id'],
392 def _paypal_s2s_get_tx_status(self, cr, uid, tx, context=None):
394 .. versionadded:: pre-v8 saas-3
397 Experimental code. You should not use it before OpenERP v8 official
400 # TDETODO: check tx.paypal_txn_id is set
402 'Content-Type': 'application/json',
403 'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
405 url = 'https://api.sandbox.paypal.com/v1/payments/payment/%s' % (tx.paypal_txn_id)
406 request = urllib2.Request(url, headers=headers)
407 data = self._paypal_try_url(request, tries=3, context=context)
408 return self.s2s_feedback(cr, uid, tx.id, data, context=context)