I already add cash_on_delivery payment method to my project. But i want to add one more method. How i can do it. At this moment i have code of checkout views like this:
class PaymentDetailsView(PaymentDetailsView):
template_name = 'checkout/payment-details.html'
template_name_preview = 'checkout/preview.html'
def get_context_data(self, **kwargs):
ctx = super(PaymentDetailsView, self).get_context_data(**kwargs)
ctx['signature'] = gateway.hmac_gen(ctx)
ctx['amount'] = '%s' % ctx['order_total'].incl_tax
ctx['price'] = '%s' % ctx['basket'].lines.all()[0].price_incl_tax
ctx['source'] = ctx
return ctx
# def handle_payment_details_submission(self, request):
# # Validate the submitted forms
# shipping_address = self.get_shipping_address(
# self.request.basket)
# address_form = BillingAddressForm(shipping_address, request.POST)
#
# if address_form.is_valid():
# address_fields = dict(
# (k, v) for (k, v) in address_form.instance.__dict__.items()
# if not k.startswith('_') and not k.startswith('same_as_shipping'))
# self.checkout_session.bill_to_new_address(address_fields)
# return self.render_preview(request, billing_address_form=address_form)
#
# # Forms are invalid - show them to the customer along with the
# # validation errors.
# return self.render_payment_details(
# request, billing_address_form=address_form)
def handle_payment(self, order_number, total, **kwargs):
reference = gateway.create_transaction(order_number, total)
source_type, is_created = SourceType.objects.get_or_create(
name='Cash on Delivery')
source = Source(
source_type=source_type,
currency=total.currency,
amount_allocated=total.incl_tax,
amount_debited=total.incl_tax
)
self.add_payment_source(source)
self.add_payment_event('Issued', total.incl_tax, reference=reference)
Maybe i can make with payment like with adding shipping methods?
Docdata was a very good inspiration for my implementation.
Multiple payment methods, means you will first have to list all methods somewhere (for easy enable/disable or something else) and then decide what methods are applicable to given user (depending on currency, basket size, location, etc.) All this can be handled by extending the PaymentMethodView
.
settings.py
from oscar import get_core_apps as get_oscar_apps
...
INSTALLED_APPS = [
...
] + get_oscar_apps([
'checkout',
]) + [
'cashondelivery', # https://github.com/ashishnitinpatil/django-oscar-cash-on-delivery
'custom_payment', # my local app for custom payment gateway
]
OSCAR_PAYMENT_METHODS = (
('cod', _('Cash on delivery')),
('custom_payment', _('Credit / Debit card')),
)
...
checkout/forms.py
from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
class PaymentMethodForm(forms.Form):
"""
Extra form for the custom payment method.
"""
payment_method = forms.ChoiceField(
label=_("Select a payment method"),
choices=settings.OSCAR_PAYMENT_METHODS,
widget=forms.RadioSelect()
)
def get_payment_method_display(payment_method):
return dict(settings.OSCAR_PAYMENT_METHODS).get(payment_method)
checkout/views.py
from django.utils import six
from django.conf import settings
from django.shortcuts import redirect
from django.views.generic import FormView
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse, reverse_lazy
from . import forms
from custom_payment import facade
from cashondelivery import gateway as cod_gateway
from oscar.apps.checkout import exceptions
from oscar.core.loading import get_model, get_class
Source = get_model("payment", "Source")
SourceType = get_model("payment", "SourceType")
RedirectRequired = get_class("payment.exceptions", "RedirectRequired")
UnableToPlaceOrder = get_class('order.exceptions', 'UnableToPlaceOrder')
OscarPaymentMethodView = get_class("checkout.views", "PaymentMethodView")
OscarPaymentDetailsView = get_class("checkout.views", "PaymentDetailsView")
OscarShippingMethodView = get_class("checkout.views", "ShippingMethodView")
# Sample pre-condition
class CheckCountryPreCondition(object):
"""DRY class for check country in session pre_condition"""
def get_pre_conditions(self, request):
if 'check_country_in_session' not in self.pre_conditions:
return self.pre_conditions + ['check_country_in_session']
return super().get_pre_conditions(request)
def check_country_in_session(self, request):
if request.session.get('country', None) is None:
raise exceptions.FailedPreCondition(
url=reverse('checkout:shipping-address'),
)
# Inspired by https://github.com/django-oscar/django-oscar-docdata/blob/master/sandbox/apps/checkout/views.py
class PaymentMethodView(CheckCountryPreCondition, OscarPaymentMethodView, FormView):
"""
View for a user to choose which payment method(s) they want to use.
This would include setting allocations if payment is to be split
between multiple sources. It's not the place for entering sensitive details
like bankcard numbers though - that belongs on the payment details view.
"""
template_name = "checkout/payment_method.html"
step = 'payment-method'
form_class = forms.PaymentMethodForm
success_url = reverse_lazy('checkout:payment-details')
pre_conditions = [
'check_basket_is_not_empty',
'check_basket_is_valid',
'check_user_email_is_captured',
'check_shipping_data_is_captured',
'check_payment_data_is_captured',
]
skip_conditions = ['skip_unless_payment_is_required']
def get(self, request, *args, **kwargs):
# if only single payment method, store that
# and then follow default (redirect to preview)
# else show payment method choice form
if len(settings.OSCAR_PAYMENT_METHODS) == 1:
self.checkout_session.pay_by(settings.OSCAR_PAYMENT_METHODS[0][0])
return redirect(self.get_success_url())
else:
return FormView.get(self, request, *args, **kwargs)
def get_success_url(self, *args, **kwargs):
# Redirect to the correct payments page as per the method (different methods may have different views &/or additional views)
return reverse_lazy('checkout:preview')
def get_initial(self):
return {
'payment_method': self.checkout_session.payment_method(),
}
def form_valid(self, form):
# Store payment method in the CheckoutSessionMixin.checkout_session (a CheckoutSessionData object)
self.checkout_session.pay_by(form.cleaned_data['payment_method'])
return super().form_valid(form)
class PaymentDetailsView(CheckCountryPreCondition, OscarPaymentDetailsView):
def handle_payment(self, order_number, order_total, **kwargs):
method = self.checkout_session.payment_method()
if method == 'cod':
return self.handle_cod_payment(order_number, order_total, **kwargs)
elif method == 'custom_payment':
return self.handle_custom_payment_payment(order_number, order_total, **kwargs)
else:
raise PaymentError(_('Bad payment method in handle_payment!'))
def handle_cod_payment(self, order_number, total, **kwargs):
reference = cod_gateway.create_transaction(order_number, total)
source_type, is_created = SourceType.objects.get_or_create(
name='Cash on delivery')
source = Source(
source_type=source_type,
currency=total.currency,
amount_allocated=total.incl_tax,
amount_debited=total.incl_tax
)
self.add_payment_source(source)
self.add_payment_event('awaiting-delivery', total.incl_tax, reference=reference)
def handle_custom_payment_payment(self, order_number, total, **kwargs):
submission = self.build_submission(order_number=order_number, **kwargs)
# Save required payment gateway data
# also validates that we have all we need
custom_payment_payment_data = facade.get_gateway_url_and_parameters(
submission, self.request.build_absolute_uri, live=settings.custom_payment['LIVE'])
# Any raised exceptions are handled by the PaymentDetail.submit() code.
custom_payment_order = facade.create_order(
order_number=order_number,
amount=total,
payment_data=custom_payment_payment_data
)
# record payment data to session to double verify things on success / failure
self.set_custom_payment_payment_data(custom_payment_payment_data, custom_payment_order)
source = Source(
source_type=facade.get_source_type(),
currency=total.currency,
amount_allocated=total.incl_tax, # amount_* field depends on type of transaction.
reference=custom_payment_order.id
)
self.add_payment_source(source)
# Also record payment event.
# This will be visible in the Dashboard
self.add_payment_event('pre-auth', total.incl_tax, reference=custom_payment_order.id)
# Regardless of whether the order is paid, write it in the database before redirecting.
# Oscar actually skips this when redirecting the user to the payment provider.
self._save_order(order_number, submission)
# Redirect the user to the payment provider's gateway.
raise RedirectRequired(facade.get_payment_url())
def _save_order(self, order_number, submission):
# Finalize the order that PaymentDetailsView.submit() started
# If all is ok with payment, try and place order
logger.info("Order #%s: payment started, placing order", order_number)
try:
# Call OrderPlacementMixin.handle_order_placement()
return self.handle_order_placement(
order_number, submission['user'], submission['basket'],
submission['shipping_address'], submission['shipping_method'],
submission['shipping_charge'], submission['billing_address'],
submission['order_total'], **(submission['order_kwargs'])
)
except UnableToPlaceOrder as e:
# It's possible that something will go wrong while trying to
# actually place an order. Not a good situation to be in as a
# payment transaction may already have taken place, but needs
# to be handled gracefully.
logger.error("Order #%s: unable to place order - %s", order_number, e, exc_info=True)
msg = six.text_type(e)
self.restore_frozen_basket()
return self.render_to_response(self.get_context_data(error=msg))
# Can't update CheckoutSessionMixin nicely without causing trouble,
# hence dumping the overridden / new methods from that class here
def check_payment_data_is_captured(self, request):
# We don't collect payment data by default so we don't have anything to
# validate here. If your shop requires forms to be submitted on the
# payment details page, then override this method to check that the
# relevant data is available. Often just enforcing that the preview
# view is only accessible from a POST request is sufficient.
if not self.checkout_session.payment_method():
raise FailedPreCondition(
url=reverse('checkout:payment-method'),
message=_("Please select a payment method for your order!")
)
def set_custom_payment_payment_data(self, payment_data, custom_payment_order):
self.request.session['custom_payment'] = payment_data
self.request.session['custom_payment_order_id'] = custom_payment_order.id
self.request.session['ongoing_online_payment'] = True
self.request.session.pop('custom_payment_error', None)
def get_context_data(self, **kwargs):
ctx = super(PaymentDetailsView, self).get_context_data(**kwargs)
ctx.update({'payment_method': self.checkout_session.payment_method()})
return ctx
def send_confirmation_message(self, order, *args, **kwargs):
# In case of custom_payment, delay sending the order confirmation till payment success!
if not self.checkout_session.payment_method() == 'custom_payment':
super(PaymentDetailsView, self).send_confirmation_message(order, *args, **kwargs)
You may ignore the details for custom_payment
since that is something specific for my project / payment gateway, you would likely have different requirements for that. I have still left a chunk of the things in it so that you may think of the things accordingly.
PS - Please note I'm using Python3.6, so a few things (like super
) might not work as mentioned here, in previous Python versions (specially 2.7)
Update
My templates/checkout/payment_method.html
:
{% extends "checkout/checkout.html" %}
{% load i18n %}
{% block checkout_title %}{{ form.payment_method.label }}{% endblock %}
{% block checkout_nav %}
{% include 'checkout/nav.html' with step=3 %}
{% endblock %}
{% block content %}
{% if error %}
<div class="alert alert-error">
{{ error }}
</div>
{% endif %}
<form action="" method="post">
{% csrf_token %}
{% if form.payment_method.errors %}{{ form.payment_method.errors }}{% endif %}
<div class="radio">
<label for="id_payment_method_0">
<input id="id_payment_method_0" name="payment_method" type="radio" value="cod" required="">{% trans "Cash on delivery" %}
</label>
</div>
<div class="radio">
<label for="id_payment_method_1">
<input id="id_payment_method_1" name="payment_method" type="radio" value="custom_payment" required="">{% trans "Credit / Debit Card" %}
</label>
</div>
<p><button type="submit" class="btn btn-large btn-primary">{% trans "Continue to confirmation" %}</button>
</form>
{% endblock %}
来源:https://stackoverflow.com/questions/49349883/how-to-add-few-payment-methods-to-django-oscar