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, environment, context=None):
26 if environment == 'prod':
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',
37 def _get_providers(self, cr, uid, context=None):
38 providers = super(AcquirerPaypal, self)._get_providers(cr, uid, context=context)
39 providers.append(['paypal', 'Paypal'])
43 'paypal_email_account': fields.char('Paypal Email ID', required_if_provider='paypal'),
44 'paypal_seller_account': fields.char(
46 help='The Merchant ID is used to ensure communications coming from Paypal are valid and secured.'),
47 'paypal_use_ipn': fields.boolean('Use IPN', help='Paypal Instant Payment Notification'),
49 'paypal_api_enabled': fields.boolean('Use Rest API'),
50 'paypal_api_username': fields.char('Rest API Username'),
51 'paypal_api_password': fields.char('Rest API Password'),
52 'paypal_api_access_token': fields.char('Access Token'),
53 'paypal_api_access_token_validity': fields.datetime('Access Token Validity'),
57 'paypal_use_ipn': True,
59 'fees_dom_fixed': 0.35,
61 'fees_int_fixed': 0.35,
63 'paypal_api_enabled': False,
66 def _migrate_paypal_account(self, cr, uid, context=None):
68 cr.execute('SELECT id, paypal_account FROM res_company')
70 for (company_id, company_paypal_account) in res:
71 if company_paypal_account:
72 company_paypal_ids = self.search(cr, uid, [('company_id', '=', company_id), ('name', '=', 'paypal')], limit=1, context=context)
73 if company_paypal_ids:
74 self.write(cr, uid, company_paypal_ids, {'paypal_email_account': company_paypal_account}, context=context)
76 paypal_view = self.pool['ir.model.data'].get_object(cr, uid, 'payment_paypal', 'paypal_acquirer_button')
77 self.create(cr, uid, {
80 'paypal_email_account': company_paypal_account,
81 'view_template_id': paypal_view.id,
85 def paypal_compute_fees(self, cr, uid, id, amount, currency_id, country_id, context=None):
86 """ Compute paypal fees.
88 :param float amount: the amount to pay
89 :param integer country_id: an ID of a res.country, or None. This is
90 the customer's country, to be compared to
91 the acquirer company country.
92 :return float fees: computed fees
94 acquirer = self.browse(cr, uid, id, context=context)
95 if not acquirer.fees_active:
97 country = self.pool['res.country'].browse(cr, uid, country_id, context=context)
98 if country and acquirer.company_id.country_id.id == country.id:
99 percentage = acquirer.fees_dom_var
100 fixed = acquirer.fees_dom_fixed
102 percentage = acquirer.fees_int_var
103 fixed = acquirer.fees_int_fixed
104 fees = (percentage / 100.0 * amount + fixed ) / (1 - percentage / 100.0)
107 def paypal_form_generate_values(self, cr, uid, id, partner_values, tx_values, context=None):
108 base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
109 acquirer = self.browse(cr, uid, id, context=context)
111 paypal_tx_values = dict(tx_values)
112 paypal_tx_values.update({
114 'business': acquirer.paypal_email_account,
115 'item_name': tx_values['reference'],
116 'item_number': tx_values['reference'],
117 'amount': tx_values['amount'],
118 'currency_code': tx_values['currency'] and tx_values['currency'].name or '',
119 'address1': partner_values['address'],
120 'city': partner_values['city'],
121 'country': partner_values['country'] and partner_values['country'].name or '',
122 'state': partner_values['state'] and partner_values['state'].name or '',
123 'email': partner_values['email'],
124 'zip': partner_values['zip'],
125 'first_name': partner_values['first_name'],
126 'last_name': partner_values['last_name'],
127 'return': '%s' % urlparse.urljoin(base_url, PaypalController._return_url),
128 'notify_url': '%s' % urlparse.urljoin(base_url, PaypalController._notify_url),
129 'cancel_return': '%s' % urlparse.urljoin(base_url, PaypalController._cancel_url),
131 if acquirer.fees_active:
132 paypal_tx_values['handling'] = '%.2f' % paypal_tx_values.pop('fees', 0.0)
133 if paypal_tx_values.get('return_url'):
134 paypal_tx_values['custom'] = json.dumps({'return_url': '%s' % paypal_tx_values.pop('return_url')})
135 return partner_values, paypal_tx_values
137 def paypal_get_form_action_url(self, cr, uid, id, context=None):
138 acquirer = self.browse(cr, uid, id, context=context)
139 return self._get_paypal_urls(cr, uid, acquirer.environment, context=context)['paypal_form_url']
141 def _paypal_s2s_get_access_token(self, cr, uid, ids, context=None):
143 Note: see # see http://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem
144 for explanation why we use Authorization header instead of urllib2
147 res = dict.fromkeys(ids, False)
148 parameters = werkzeug.url_encode({'grant_type': 'client_credentials'})
150 for acquirer in self.browse(cr, uid, ids, context=context):
151 tx_url = self._get_paypal_urls(cr, uid, acquirer.environment)['paypal_rest_url']
152 request = urllib2.Request(tx_url, parameters)
154 # add other headers (https://developer.paypal.com/webapps/developer/docs/integration/direct/make-your-first-call/)
155 request.add_header('Accept', 'application/json')
156 request.add_header('Accept-Language', 'en_US')
158 # add authorization header
159 base64string = base64.encodestring('%s:%s' % (
160 acquirer.paypal_api_username,
161 acquirer.paypal_api_password)
163 request.add_header("Authorization", "Basic %s" % base64string)
165 request = urllib2.urlopen(request)
166 result = request.read()
167 res[acquirer.id] = json.loads(result).get('access_token')
172 class TxPaypal(osv.Model):
173 _inherit = 'payment.transaction'
176 'paypal_txn_id': fields.char('Transaction ID'),
177 'paypal_txn_type': fields.char('Transaction type'),
180 # --------------------------------------------------
181 # FORM RELATED METHODS
182 # --------------------------------------------------
184 def _paypal_form_get_tx_from_data(self, cr, uid, data, context=None):
185 reference, txn_id = data.get('item_number'), data.get('txn_id')
186 if not reference or not txn_id:
187 error_msg = 'Paypal: received data with missing reference (%s) or txn_id (%s)' % (reference, txn_id)
188 _logger.error(error_msg)
189 raise ValidationError(error_msg)
191 # find tx -> @TDENOTE use txn_id ?
192 tx_ids = self.pool['payment.transaction'].search(cr, uid, [('reference', '=', reference)], context=context)
193 if not tx_ids or len(tx_ids) > 1:
194 error_msg = 'Paypal: received data for reference %s' % (reference)
196 error_msg += '; no order found'
198 error_msg += '; multiple order found'
199 _logger.error(error_msg)
200 raise ValidationError(error_msg)
201 return self.browse(cr, uid, tx_ids[0], context=context)
203 def _paypal_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
204 invalid_parameters = []
205 if data.get('notify_version')[0] != '3.4':
207 'Received a notification from Paypal with version %s instead of 2.6. This could lead to issues when managing it.' %
208 data.get('notify_version')
210 if data.get('test_ipn'):
212 'Received a notification from Paypal using sandbox'
215 # TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details
216 if tx.acquirer_reference and data.get('txn_id') != tx.acquirer_reference:
217 invalid_parameters.append(('txn_id', data.get('txn_id'), tx.acquirer_reference))
218 # check what is buyed
219 if float_compare(float(data.get('mc_gross', '0.0')), (tx.amount + tx.fees), 2) != 0:
220 invalid_parameters.append(('mc_gross', data.get('mc_gross'), '%.2f' % tx.amount)) # mc_gross is amount + fees
221 if data.get('mc_currency') != tx.currency_id.name:
222 invalid_parameters.append(('mc_currency', data.get('mc_currency'), tx.currency_id.name))
223 if 'handling_amount' in data and float_compare(float(data.get('handling_amount')), tx.fees, 2) != 0:
224 invalid_parameters.append(('handling_amount', data.get('handling_amount'), tx.fees))
226 if tx.partner_reference and data.get('payer_id') != tx.partner_reference:
227 invalid_parameters.append(('payer_id', data.get('payer_id'), tx.partner_reference))
229 if data.get('receiver_email') != tx.acquirer_id.paypal_email_account:
230 invalid_parameters.append(('receiver_email', data.get('receiver_email'), tx.acquirer_id.paypal_email_account))
231 if data.get('receiver_id') and tx.acquirer_id.paypal_seller_account and data['receiver_id'] != tx.acquirer_id.paypal_seller_account:
232 invalid_parameters.append(('receiver_id', data.get('receiver_id'), tx.acquirer_id.paypal_seller_account))
234 return invalid_parameters
236 def _paypal_form_validate(self, cr, uid, tx, data, context=None):
237 status = data.get('payment_status')
239 'acquirer_reference': data.get('txn_id'),
240 'paypal_txn_type': data.get('payment_type'),
241 'partner_reference': data.get('payer_id')
243 if status in ['Completed', 'Processed']:
244 _logger.info('Validated Paypal payment for tx %s: set as done' % (tx.reference))
245 data.update(state='done', date_validate=data.get('payment_date', fields.datetime.now()))
246 return tx.write(data)
247 elif status in ['Pending', 'Expired']:
248 _logger.info('Received notification for Paypal payment %s: set as pending' % (tx.reference))
249 data.update(state='pending', state_message=data.get('pending_reason', ''))
250 return tx.write(data)
252 error = 'Received unrecognized status for Paypal payment %s: %s, set as error' % (tx.reference, status)
254 data.update(state='error', state_message=error)
255 return tx.write(data)
257 # --------------------------------------------------
258 # SERVER2SERVER RELATED METHODS
259 # --------------------------------------------------
261 def _paypal_try_url(self, request, tries=3, context=None):
262 """ Try to contact Paypal. Due to some issues, internal service errors
263 seem to be quite frequent. Several tries are done before considering
264 the communication as failed.
266 .. versionadded:: pre-v8 saas-3
269 Experimental code. You should not use it before OpenERP v8 official
272 done, res = False, None
273 while (not done and tries):
275 res = urllib2.urlopen(request)
277 except urllib2.HTTPError as e:
280 if tries and res and json.loads(res)['name'] == 'INTERNAL_SERVICE_ERROR':
281 _logger.warning('Failed contacting Paypal, retrying (%s remaining)' % tries)
285 # raise openerp.exceptions.
290 def _paypal_s2s_send(self, cr, uid, values, cc_values, context=None):
292 .. versionadded:: pre-v8 saas-3
295 Experimental code. You should not use it before OpenERP v8 official
298 tx_id = self.create(cr, uid, values, context=context)
299 tx = self.browse(cr, uid, tx_id, context=context)
302 'Content-Type': 'application/json',
303 'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
309 'total': '%.2f' % tx.amount,
310 'currency': tx.currency_id.name,
312 'description': tx.reference,
317 'payment_method': 'credit_card',
318 'funding_instruments': [{
320 'number': cc_values['number'],
321 'type': cc_values['brand'],
322 'expire_month': cc_values['expiry_mm'],
323 'expire_year': cc_values['expiry_yy'],
324 'cvv2': cc_values['cvc'],
325 'first_name': tx.partner_name,
326 'last_name': tx.partner_name,
328 'line1': tx.partner_address,
329 'city': tx.partner_city,
330 'country_code': tx.partner_country_id.code,
331 'postal_code': tx.partner_zip,
337 # TODO: complete redirect URLs
338 data['redirect_urls'] = {
339 # 'return_url': 'http://example.com/your_redirect_url/',
340 # 'cancel_url': 'http://example.com/your_cancel_url/',
343 'payment_method': 'paypal',
345 data = json.dumps(data)
347 request = urllib2.Request('https://api.sandbox.paypal.com/v1/payments/payment', data, headers)
348 result = self._paypal_try_url(request, tries=3, context=context)
349 return (tx_id, result)
351 def _paypal_s2s_get_invalid_parameters(self, cr, uid, tx, data, context=None):
353 .. versionadded:: pre-v8 saas-3
356 Experimental code. You should not use it before OpenERP v8 official
359 invalid_parameters = []
360 return invalid_parameters
362 def _paypal_s2s_validate(self, cr, uid, tx, data, context=None):
364 .. versionadded:: pre-v8 saas-3
367 Experimental code. You should not use it before OpenERP v8 official
370 values = json.loads(data)
371 status = values.get('state')
372 if status in ['approved']:
373 _logger.info('Validated Paypal s2s payment for tx %s: set as done' % (tx.reference))
376 'date_validate': values.get('udpate_time', fields.datetime.now()),
377 'paypal_txn_id': values['id'],
380 elif status in ['pending', 'expired']:
381 _logger.info('Received notification for Paypal s2s payment %s: set as pending' % (tx.reference))
384 # 'state_message': data.get('pending_reason', ''),
385 'paypal_txn_id': values['id'],
389 error = 'Received unrecognized status for Paypal s2s payment %s: %s, set as error' % (tx.reference, status)
393 # 'state_message': error,
394 'paypal_txn_id': values['id'],
398 def _paypal_s2s_get_tx_status(self, cr, uid, tx, context=None):
400 .. versionadded:: pre-v8 saas-3
403 Experimental code. You should not use it before OpenERP v8 official
406 # TDETODO: check tx.paypal_txn_id is set
408 'Content-Type': 'application/json',
409 'Authorization': 'Bearer %s' % tx.acquirer_id._paypal_s2s_get_access_token()[tx.acquirer_id.id],
411 url = 'https://api.sandbox.paypal.com/v1/payments/payment/%s' % (tx.paypal_txn_id)
412 request = urllib2.Request(url, headers=headers)
413 data = self._paypal_try_url(request, tries=3, context=context)
414 return self.s2s_feedback(cr, uid, tx.id, data, context=context)