Files
odoo_source/addons/payment_stripe/models/payment.py
T
qdp-odoo 01216345e2 [REF] account,payment(_*),sale*: downgrade transactions into debug items
It was very confusing for the user to distinct account.payment and payment.transaction. From now on, the transactions are
technical objects and, in the backend, we only refer to it in log messages (Front end will be adapted in the same fashion
later on). They are hidden in debug mode in accounting\configuration\payments as their purpose is now purely technical/log

This commit also aims to reduce the gap between the accounting app and the transactions: account.payment objects are
created/validated upon completion of transaction.

To ease the capture/voiding of pending transactions, the related buttons are now displayed directly on the SO/invoice
instead of the transactions.

Was task: https://www.odoo.com/web#id=35857&view_type=form&model=project.task&action=333&active_id=967&menu_id=4720
Was PR #24043

[FIX] add domain based on journal to payment tokens

Was opw: https://www.odoo.com/web?debug#id=1828206&view_type=form&model=project.task&menu_id=5200
2018-05-23 15:44:55 +02:00

309 lines
13 KiB
Python

# coding: utf-8
import logging
import requests
import pprint
from odoo import api, fields, models, _
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.exceptions import UserError
from odoo.tools.safe_eval import safe_eval
from odoo.tools.float_utils import float_round
_logger = logging.getLogger(__name__)
# Force the API version to avoid breaking in case of update on Stripe side
# cf https://stripe.com/docs/api#versioning
# changelog https://stripe.com/docs/upgrades#api-changelog
STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'}
# The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal
INT_CURRENCIES = [
u'BIF', u'XAF', u'XPF', u'CLP', u'KMF', u'DJF', u'GNF', u'JPY', u'MGA', u'PYG', u'RWF', u'KRW',
u'VUV', u'VND', u'XOF'
]
class PaymentAcquirerStripe(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(selection_add=[('stripe', 'Stripe')])
stripe_secret_key = fields.Char(required_if_provider='stripe', groups='base.group_user')
stripe_publishable_key = fields.Char(required_if_provider='stripe', groups='base.group_user')
stripe_image_url = fields.Char(
"Checkout Image URL", groups='base.group_user',
help="A relative or absolute URL pointing to a square image of your "
"brand or product. As defined in your Stripe profile. See: "
"https://stripe.com/docs/checkout")
@api.multi
def stripe_form_generate_values(self, tx_values):
self.ensure_one()
stripe_tx_values = dict(tx_values)
temp_stripe_tx_values = {
'company': self.company_id.name,
'amount': tx_values['amount'], # Mandatory
'currency': tx_values['currency'].name, # Mandatory anyway
'currency_id': tx_values['currency'].id, # same here
'address_line1': tx_values.get('partner_address'), # Any info of the partner is not mandatory
'address_city': tx_values.get('partner_city'),
'address_country': tx_values.get('partner_country') and tx_values.get('partner_country').name or '',
'email': tx_values.get('partner_email'),
'address_zip': tx_values.get('partner_zip'),
'name': tx_values.get('partner_name'),
'phone': tx_values.get('partner_phone'),
}
temp_stripe_tx_values['returndata'] = stripe_tx_values.pop('return_url', '')
stripe_tx_values.update(temp_stripe_tx_values)
return stripe_tx_values
@api.model
def _get_stripe_api_url(self):
return 'api.stripe.com/v1'
@api.model
def stripe_s2s_form_process(self, data):
payment_token = self.env['payment.token'].sudo().create({
'cc_number': data['cc_number'],
'cc_holder_name': data['cc_holder_name'],
'cc_expiry': data['cc_expiry'],
'cc_brand': data['cc_brand'],
'cvc': data['cvc'],
'acquirer_id': int(data['acquirer_id']),
'partner_id': int(data['partner_id'])
})
return payment_token
@api.multi
def stripe_s2s_form_validate(self, data):
self.ensure_one()
# mandatory fields
for field_name in ["cc_number", "cvc", "cc_holder_name", "cc_expiry", "cc_brand"]:
if not data.get(field_name):
return False
return True
def _get_feature_support(self):
"""Get advanced feature support by provider.
Each provider should add its technical in the corresponding
key for the following features:
* fees: support payment fees computations
* authorize: support authorizing payment (separates
authorization and capture)
* tokenize: support saving payment data in a payment.tokenize
object
"""
res = super(PaymentAcquirerStripe, self)._get_feature_support()
res['tokenize'].append('stripe')
return res
class PaymentTransactionStripe(models.Model):
_inherit = 'payment.transaction'
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None):
api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url())
charge_params = {
'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else float_round(self.amount * 100, 2)),
'currency': self.currency_id.name,
'metadata[reference]': self.reference,
'description': self.reference,
}
if acquirer_ref:
charge_params['customer'] = acquirer_ref
if tokenid:
charge_params['card'] = str(tokenid)
if email:
charge_params['receipt_email'] = email.strip()
_logger.info('_create_stripe_charge: Sending values to URL %s, values:\n%s', api_url_charge, pprint.pformat(charge_params))
r = requests.post(api_url_charge,
auth=(self.acquirer_id.stripe_secret_key, ''),
params=charge_params,
headers=STRIPE_HEADERS)
res = r.json()
_logger.info('_create_stripe_charge: Values received:\n%s', pprint.pformat(res))
return res
@api.multi
def stripe_s2s_do_transaction(self, **kwargs):
self.ensure_one()
result = self._create_stripe_charge(acquirer_ref=self.payment_token_id.acquirer_ref, email=self.partner_email)
return self._stripe_s2s_validate_tree(result)
def _create_stripe_refund(self):
api_url_refund = 'https://%s/refunds' % (self.acquirer_id._get_stripe_api_url())
refund_params = {
'charge': self.acquirer_reference,
'amount': int(float_round(self.amount * 100, 2)), # by default, stripe refund the full amount (we don't really need to specify the value)
'metadata[reference]': self.reference,
}
_logger.info('_create_stripe_refund: Sending values to URL %s, values:\n%s', api_url_refund, pprint.pformat(refund_params))
r = requests.post(api_url_refund,
auth=(self.acquirer_id.stripe_secret_key, ''),
params=refund_params,
headers=STRIPE_HEADERS)
res = r.json()
_logger.info('_create_stripe_refund: Values received:\n%s', pprint.pformat(res))
return res
@api.multi
def stripe_s2s_do_refund(self, **kwargs):
self.ensure_one()
self.state = 'refunding'
result = self._create_stripe_refund()
return self._stripe_s2s_validate_tree(result)
@api.model
def _stripe_form_get_tx_from_data(self, data):
""" Given a data dict coming from stripe, verify it and find the related
transaction record. """
reference = data.get('metadata', {}).get('reference')
if not reference:
stripe_error = data.get('error', {}).get('message', '')
_logger.error('Stripe: invalid reply received from stripe API, looks like '
'the transaction failed. (error: %s)', stripe_error or 'n/a')
error_msg = _("We're sorry to report that the transaction has failed.")
if stripe_error:
error_msg += " " + (_("Stripe gave us the following info about the problem: '%s'") %
stripe_error)
error_msg += " " + _("Perhaps the problem can be solved by double-checking your "
"credit card details, or contacting your bank?")
raise ValidationError(error_msg)
tx = self.search([('reference', '=', reference)])
if not tx:
error_msg = (_('Stripe: no order found for reference %s') % reference)
_logger.error(error_msg)
raise ValidationError(error_msg)
elif len(tx) > 1:
error_msg = (_('Stripe: %s orders found for reference %s') % (len(tx), reference))
_logger.error(error_msg)
raise ValidationError(error_msg)
return tx[0]
@api.multi
def _stripe_s2s_validate_tree(self, tree):
self.ensure_one()
if self.state != 'draft':
_logger.info('Stripe: trying to validate an already validated tx (ref %s)', self.reference)
return True
status = tree.get('status')
if status == 'succeeded':
self.write({
'date': fields.datetime.now(),
'acquirer_reference': tree.get('id'),
})
self._set_transaction_done()
self.execute_callback()
if self.payment_token_id:
self.payment_token_id.verified = True
return True
else:
error = tree['error']['message']
_logger.warn(error)
self.sudo().write({
'state_message': error,
'acquirer_reference': tree.get('id'),
'date': fields.datetime.now(),
})
self._set_transaction_cancel()
return False
@api.multi
def _stripe_form_get_invalid_parameters(self, data):
invalid_parameters = []
reference = data['metadata']['reference']
if reference != self.reference:
invalid_parameters.append(('Reference', reference, self.reference))
return invalid_parameters
@api.multi
def _stripe_form_validate(self, data):
return self._stripe_s2s_validate_tree(data)
class PaymentTokenStripe(models.Model):
_inherit = 'payment.token'
@api.model
def stripe_create(self, values):
token = values.get('stripe_token')
description = None
payment_acquirer = self.env['payment.acquirer'].browse(values.get('acquirer_id'))
# when asking to create a token on Stripe servers
if values.get('cc_number'):
url_token = 'https://%s/tokens' % payment_acquirer._get_stripe_api_url()
payment_params = {
'card[number]': values['cc_number'].replace(' ', ''),
'card[exp_month]': str(values['cc_expiry'][:2]),
'card[exp_year]': str(values['cc_expiry'][-2:]),
'card[cvc]': values['cvc'],
'card[name]': values['cc_holder_name'],
}
r = requests.post(url_token,
auth=(payment_acquirer.stripe_secret_key, ''),
params=payment_params,
headers=STRIPE_HEADERS)
token = r.json()
description = values['cc_holder_name']
else:
partner_id = self.env['res.partner'].browse(values['partner_id'])
description = 'Partner: %s (id: %s)' % (partner_id.name, partner_id.id)
if not token:
raise Exception('stripe_create: No token provided!')
res = self._stripe_create_customer(token, description, payment_acquirer.id)
# pop credit card info to info sent to create
for field_name in ["cc_number", "cvc", "cc_holder_name", "cc_expiry", "cc_brand", "stripe_token"]:
values.pop(field_name, None)
return res
def _stripe_create_customer(self, token, description=None, acquirer_id=None):
if token.get('error'):
_logger.error('payment.token.stripe_create_customer: Token error:\n%s', pprint.pformat(token['error']))
raise Exception(token['error']['message'])
if token['object'] != 'token':
_logger.error('payment.token.stripe_create_customer: Cannot create a customer for object type "%s"', token.get('object'))
raise Exception('We are unable to process your credit card information.')
if token['type'] != 'card':
_logger.error('payment.token.stripe_create_customer: Cannot create a customer for token type "%s"', token.get('type'))
raise Exception('We are unable to process your credit card information.')
payment_acquirer = self.env['payment.acquirer'].browse(acquirer_id or self.acquirer_id.id)
url_customer = 'https://%s/customers' % payment_acquirer._get_stripe_api_url()
customer_params = {
'source': token['id'],
'description': description or token["card"]["name"]
}
r = requests.post(url_customer,
auth=(payment_acquirer.stripe_secret_key, ''),
params=customer_params,
headers=STRIPE_HEADERS)
customer = r.json()
if customer.get('error'):
_logger.error('payment.token.stripe_create_customer: Customer error:\n%s', pprint.pformat(customer['error']))
raise Exception(customer['error']['message'])
values = {
'acquirer_ref': customer['id'],
'name': 'XXXXXXXXXXXX%s - %s' % (token['card']['last4'], customer_params["description"])
}
return values