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, environment, context=None):
27 if environment == 'prod':
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',
38 def _get_providers(self, cr, uid, context=None):
39 providers = super(AcquirerPaypal, self)._get_providers(cr, uid, context=context)
40 providers.append(['paypal', 'Paypal'])
44 'paypal_email_account': fields.char('Paypal Email ID', required_if_provider='paypal'),
45 'paypal_seller_account': fields.char(
47 help='The Merchant ID is used to ensure communications coming from Paypal are valid and secured.'),
48 'paypal_use_ipn': fields.boolean('Use IPN', help='Paypal Instant Payment Notification'),
50 'paypal_api_enabled': fields.boolean('Use Rest API'),
51 'paypal_api_username': fields.char('Rest API Username'),
52 'paypal_api_password': fields.char('Rest API Password'),
53 'paypal_api_access_token': fields.char('Access Token'),
54 'paypal_api_access_token_validity': fields.datetime('Access Token Validity'),
58 'paypal_use_ipn': True,
60 'fees_dom_fixed': 0.35,
62 'fees_int_fixed': 0.35,
64 'paypal_api_enabled': False,
67 def _migrate_paypal_account(self, cr, uid, context=None):
69 cr.execute('SELECT id, paypal_account FROM res_company')
71 for (company_id, company_paypal_account) in res:
72 if company_paypal_account:
73 company_paypal_ids = self.search(cr, uid, [('company_id', '=', company_id), ('provider', '=', 'paypal')], limit=1, context=context)
74 if company_paypal_ids:
75 self.write(cr, uid, company_paypal_ids, {'paypal_email_account': company_paypal_account}, context=context)
77 paypal_view = self.pool['ir.model.data'].get_object(cr, uid, 'payment_paypal', 'paypal_acquirer_button')
78 self.create(cr, uid, {
81 'paypal_email_account': company_paypal_account,
82 'view_template_id': paypal_view.id,
86 def paypal_compute_fees(self, cr, uid, id, amount, currency_id, country_id, context=None):
87 """ Compute paypal fees.
89 :param float amount: the amount to pay
90 :param integer country_id: an ID of a res.country, or None. This is
91 the customer's country, to be compared to
92 the acquirer company country.
93 :return float fees: computed fees
95 acquirer = self.browse(cr, uid, id, context=context)
96 if not acquirer.fees_active:
98 country = self.pool['res.country'].browse(cr, uid, country_id, context=context)
99 if country and acquirer.company_id.country_id.id == country.id:
100 percentage = acquirer.fees_dom_var
101 fixed = acquirer.fees_dom_fixed
103 percentage = acquirer.fees_int_var
104 fixed = acquirer.fees_int_fixed
105 fees = (percentage / 100.0 * amount + fixed ) / (1 - percentage / 100.0)
108 def paypal_form_generate_values(self, cr, uid, id, partner_values, tx_values, context=None):
109 base_url = self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'web.base.url')
110 acquirer = self.browse(cr, uid, id, context=context)
112 paypal_tx_values = dict(tx_values)
113 paypal_tx_values.update({
115 'business': acquirer.paypal_email_account,
116 'item_name': tx_values['reference'],
117 'item_number': tx_values['reference'],
118 'amount': tx_values['amount'],
119 'currency_code': tx_values['currency'] and tx_values['currency'].name or '',
120 'address1': partner_values['address'],
121 'city': partner_values['city'],
122 'country': partner_values['country'] and partner_values['country'].name or '',
123 'state': partner_values['state'] and partner_values['state'].name or '',
124 'email': partner_values['email'],
125 'zip': partner_values['zip'],
126 'first_name': partner_values['first_name'],
127 'last_name': partner_values['last_name'],
128 'return': '%s' % urlparse.urljoin(base_url, PaypalController._return_url),
129 'notify_url': '%s' % urlparse.urljoin(base_url, PaypalController._notify_url),
130 'cancel_return': '%s' % urlparse.urljoin(base_url, PaypalController._cancel_url),
132 if acquirer.fees_active:
133 paypal_tx_values['handling'] = '%.2f' % paypal_tx_values.pop('fees', 0.0)
134 if paypal_tx_values.get('return_url'):
135 paypal_tx_values['custom'] = json.dumps({'return_url': '%s' % paypal_tx_values.pop('return_url')})
136 return partner_values, paypal_tx_values
138 def paypal_get_form_action_url(self, cr, uid, id, context=None):
139 acquirer = self.browse(cr, uid, id, context=context)
140 return self._get_paypal_urls(cr, uid, acquirer.environment, context=context)['paypal_form_url']
142 def _paypal_s2s_get_access_token(self, cr, uid, ids, context=None):
144 Note: see # see http://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem
145 for explanation why we use Authorization header instead of urllib2
148 res = dict.fromkeys(ids, False)
149 parameters = werkzeug.url_encode({'grant_type': 'client_credentials'})
151 for acquirer in self.browse(cr, uid, ids, context=context):
152 tx_url = self._get_paypal_urls(cr, uid, acquirer.environment)['paypal_rest_url']
153 request = urllib2.Request(tx_url, parameters)
155 # add other headers (https://developer.paypal.com/webapps/developer/docs/integration/direct/make-your-first-call/)
156 request.add_header('Accept', 'application/json')
157 request.add_header('Accept-Language', 'en_US')
159 # add authorization header
160 base64string = base64.encodestring('%s:%s' % (
161 acquirer.paypal_api_username,
162 acquirer.paypal_api_password)
164 request.add_header("Authorization", "Basic %s" % base64string)
166 request = urllib2.urlopen(request)
167 result = request.read()
168 res[acquirer.id] = json.loads(result).get('access_token')
173 class TxPaypal(osv.Model):
174 _inherit = 'payment.transaction'
177 'paypal_txn_id': fields.char('Transaction ID'),
178 'paypal_txn_type': fields.char('Transaction type'),
181 # --------------------------------------------------
182 # FORM RELATED METHODS
183 # --------------------------------------------------
185 def _paypal_form_get_tx_from_data(self, cr, uid, data, context=None):
186 reference, txn_id = data.get('item_number'), data.get('txn_id')
187 if not reference or not txn_id:
188 error_msg = 'Paypal: received data with missing reference (%s) or txn_id (%s)' % (reference, txn_id)
189 _logger.error(error_msg)
190 raise ValidationError(error_msg)
192 # find tx -> @TDENOTE use txn_id ?
193 tx_ids = self.pool['payment.transaction'].search(cr, uid, [('reference', '=', reference)], context=context)
194 if not tx_ids or len(tx_ids) > 1:
195 error_msg = 'Paypal: received data for reference %s' % (reference)
197 error_msg += '; no order found'
199 error_msg += '; multiple order found'
200 _logger.error(error_msg)
201 raise ValidationError(error_msg)
202 return self.browse(cr, uid, tx_ids[0], context=context)
204 def _paypal_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
205 invalid_parameters = []
206 if data.get('notify_version')[0] != '3.4':
208 'Received a notification from Paypal with version %s instead of 2.6. This could lead to issues when managing it.' %
209 data.get('notify_version')
211 if data.get('test_ipn'):
213 'Received a notification from Paypal using sandbox'
216 # TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details
217 if tx.acquirer_reference and data.get('txn_id') != tx.acquirer_reference:
218 invalid_parameters.append(('txn_id', data.get('txn_id'), tx.acquirer_reference))
219 # check what is buyed
220 if float_compare(float(data.get('mc_gross', '0.0')), (tx.amount + tx.fees), 2) != 0:
221 invalid_parameters.append(('mc_gross', data.get('mc_gross'), '%.2f' % tx.amount)) # mc_gross is amount + fees
222 if data.get('mc_currency') != tx.currency_id.name:
223 invalid_parameters.append(('mc_currency', data.get('mc_currency'), tx.currency_id.name))
224 if 'handling_amount' in data and float_compare(float(data.get('handling_amount')), tx.fees, 2) != 0:
225 invalid_parameters.append(('handling_amount', data.get('handling_amount'), tx.fees))
227 if tx.partner_reference and data.get('payer_id') != tx.partner_reference:
228 invalid_parameters.append(('payer_id', data.get('payer_id'), tx.partner_reference))
230 if data.get('receiver_email') != tx.acquirer_id.paypal_email_account:
231 invalid_parameters.append(('receiver_email', data.get('receiver_email'), tx.acquirer_id.paypal_email_account))
232 if data.get('receiver_id') and tx.acquirer_id.paypal_seller_account and data['receiver_id'] != tx.acquirer_id.paypal_seller_account:
233 invalid_parameters.append(('receiver_id', data.get('receiver_id'), tx.acquirer_id.paypal_seller_account))
235 return invalid_parameters
237 def _paypal_form_validate(self, cr, uid, tx, data, context=None):
238 status = data.get('payment_status')
240 'acquirer_reference': data.get('txn_id'),
241 'paypal_txn_type': data.get('payment_type'),
242 'partner_reference': data.get('payer_id')
244 if status in ['Completed', 'Processed']:
245 _logger.info('Validated Paypal payment for tx %s: set as done' % (tx.reference))
246 data.update(state='done', date_validate=data.get('payment_date', fields.datetime.now()))
247 return tx.write(data)
248 elif status in ['Pending', 'Expired']:
249 _logger.info('Received notification for Paypal payment %s: set as pending' % (tx.reference))
250 data.update(state='pending', state_message=data.get('pending_reason', ''))
251 return tx.write(data)
253 error = 'Received unrecognized status for Paypal payment %s: %s, set as error' % (tx.reference, status)
255 data.update(state='error', state_message=error)
256 return tx.write(data)
258 # --------------------------------------------------
259 # SERVER2SERVER RELATED METHODS
260 # --------------------------------------------------
262 def _paypal_try_url(self, request, tries=3, context=None):
263 """ Try to contact Paypal. Due to some issues, internal service errors
264 seem to be quite frequent. Several tries are done before considering
265 the communication as failed.
267 .. versionadded:: pre-v8 saas-3
270 Experimental code. You should not use it before OpenERP v8 official
273 done, res = False, None
274 while (not done and tries):
276 res = urllib2.urlopen(request)
278 except urllib2.HTTPError as e:
281 if tries and res and json.loads(res)['name'] == 'INTERNAL_SERVICE_ERROR':
282 _logger.warning('Failed contacting Paypal, retrying (%s remaining)' % tries)
286 # raise openerp.exceptions.
291 def _paypal_s2s_send(self, cr, uid, values, cc_values, context=None):
293 .. versionadded:: pre-v8 saas-3
296 Experimental code. You should not use it before OpenERP v8 official
299 tx_id = self.create(cr, uid, values, context=context)
300 tx = self.browse(cr, uid, tx_id, context=context)
303 'Content-Type': 'application/json',
304 'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
310 'total': '%.2f' % tx.amount,
311 'currency': tx.currency_id.name,
313 'description': tx.reference,
318 'payment_method': 'credit_card',
319 'funding_instruments': [{
321 'number': cc_values['number'],
322 'type': cc_values['brand'],
323 'expire_month': cc_values['expiry_mm'],
324 'expire_year': cc_values['expiry_yy'],
325 'cvv2': cc_values['cvc'],
326 'first_name': tx.partner_name,
327 'last_name': tx.partner_name,
329 'line1': tx.partner_address,
330 'city': tx.partner_city,
331 'country_code': tx.partner_country_id.code,
332 'postal_code': tx.partner_zip,
338 # TODO: complete redirect URLs
339 data['redirect_urls'] = {
340 # 'return_url': 'http://example.com/your_redirect_url/',
341 # 'cancel_url': 'http://example.com/your_cancel_url/',
344 'payment_method': 'paypal',
346 data = json.dumps(data)
348 request = urllib2.Request('https://api.sandbox.paypal.com/v1/payments/payment', data, headers)
349 result = self._paypal_try_url(request, tries=3, context=context)
350 return (tx_id, result)
352 def _paypal_s2s_get_invalid_parameters(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 invalid_parameters = []
361 return invalid_parameters
363 def _paypal_s2s_validate(self, cr, uid, tx, data, context=None):
365 .. versionadded:: pre-v8 saas-3
368 Experimental code. You should not use it before OpenERP v8 official
371 values = json.loads(data)
372 status = values.get('state')
373 if status in ['approved']:
374 _logger.info('Validated Paypal s2s payment for tx %s: set as done' % (tx.reference))
377 'date_validate': values.get('udpate_time', fields.datetime.now()),
378 'paypal_txn_id': values['id'],
381 elif status in ['pending', 'expired']:
382 _logger.info('Received notification for Paypal s2s payment %s: set as pending' % (tx.reference))
385 # 'state_message': data.get('pending_reason', ''),
386 'paypal_txn_id': values['id'],
390 error = 'Received unrecognized status for Paypal s2s payment %s: %s, set as error' % (tx.reference, status)
394 # 'state_message': error,
395 'paypal_txn_id': values['id'],
399 def _paypal_s2s_get_tx_status(self, cr, uid, tx, context=None):
401 .. versionadded:: pre-v8 saas-3
404 Experimental code. You should not use it before OpenERP v8 official
407 # TDETODO: check tx.paypal_txn_id is set
409 'Content-Type': 'application/json',
410 'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
412 url = 'https://api.sandbox.paypal.com/v1/payments/payment/%s' % (tx.paypal_txn_id)
413 request = urllib2.Request(url, headers=headers)
414 data = self._paypal_try_url(request, tries=3, context=context)
415 return self.s2s_feedback(cr, uid, tx.id, data, context=context)