from django.conf import settings from django import forms from urllib.parse import urlencode import re from io import StringIO from postgresqleu.util.widgets import StaticTextWidget from postgresqleu.paypal.models import TransactionInfo from postgresqleu.paypal.util import PaypalAPI from postgresqleu.invoices.backendforms import BackendInvoicePaymentMethodForm from postgresqleu.accounting.util import get_account_choices from . import BasePayment class BackendPaypalForm(BackendInvoicePaymentMethodForm): sandbox = forms.BooleanField(required=False, help_text="Use testing sandbox") email = forms.EmailField(required=True, label="Paypal account email") clientid = forms.CharField(required=True, label="Client ID", widget=forms.widgets.PasswordInput(render_value=True), help_text='From developer.paypal.com, create app, set permissions') clientsecret = forms.CharField(required=True, label="Client secret", widget=forms.widgets.PasswordInput(render_value=True), help_text="From developer.paypal.com, app config") pdt_token = forms.CharField(required=True, label="PDT token", widget=forms.widgets.PasswordInput(render_value=True), help_text="Settings -> My Selling Tools -> Website preferences -> Payment data transfer") donation_text = forms.CharField(required=True, help_text="Payments with this text will be auto-matched as donations") report_receiver = forms.EmailField(required=True) accounting_income = forms.ChoiceField(required=True, choices=get_account_choices, label="Income account") accounting_fee = forms.ChoiceField(required=True, choices=get_account_choices, label="Fees account") accounting_transfer = forms.ChoiceField(required=True, choices=get_account_choices, label="Transfer account", help_text="Account that transfers from paypal are made to") returnurl = forms.CharField(label="Return URL", widget=StaticTextWidget) config_fields = ['sandbox', 'email', 'clientid', 'clientsecret', 'pdt_token', 'donation_text', 'report_receiver', 'accounting_income', 'accounting_fee', 'accounting_transfer', 'returnurl', ] config_readonly = ['returnurl', ] config_fieldsets = [ { 'id': 'paypal', 'legend': 'Paypal', 'fields': ['email', 'sandbox', 'clientid', 'clientsecret', 'pdt_token'], }, { 'id': 'integration', 'legend': 'Integration', 'fields': ['report_receiver', 'donation_text', ], }, { 'id': 'accounting', 'legend': 'Accounting', 'fields': ['accounting_income', 'accounting_fee', 'accounting_transfer'], }, { 'id': 'paypalconf', 'legend': 'Paypal configuration', 'fields': ['returnurl', ], } ] def fix_fields(self): super(BackendPaypalForm, self).fix_fields() if self.instance.id: self.initial.update({ 'returnurl': """ On the Paypal account, go into Settings, then My Selling Tools, then Website Preferences. Make sure that Auto Return is enabled, and enter the url {0}/p/paypal_return/{1}/""".format( settings.SITEBASE, self.instance.id, ), }) @classmethod def validate_data_for(self, instance): pm = instance.get_implementation() api = PaypalAPI(pm) s = StringIO() api.ensure_access_token() s.write("Paypal API access credentials correct,\n") if 'https://uri.paypal.com/services/reporting/search/read ' not in api.tokenscope: s.write("Paypal token does NOT have have Transaction search permissions!\n") if 'https://api.paypal.com/v1/payments/.* ' not in api.tokenscope: s.write("Paypal token does NOT have have Accept payments permissions!\n") return s.getvalue() class Paypal(BasePayment): backend_form_class = BackendPaypalForm description = """ Pay using Paypal. You can use this both to pay from your Paypal balance if you have a Paypal account, or you can use it to pay with any credit card supported by Paypal (Visa, Mastercard, American Express). In most countries, you do not need a Paypal account if you choose to pay with credit card. However, we do recommend using the payment method called "Credit card" instead of Paypal if you are paying with a credit card, as it has lower fees. """ PAYPAL_COMMON = { 'lc': 'GB', 'currency_code': settings.CURRENCY_ABBREV, 'button_subtype': 'services', 'no_note': '1', 'no_shipping': '1', 'bn': 'PP-BuyNowBF:btn_buynowCC_LG.gif-NonHosted', 'charset': 'utf-8', } def get_baseurl(self): if self.config('sandbox'): return 'https://www.sandbox.paypal.com/cgi-bin/webscr' return 'https://www.paypal.com/cgi-bin/webscr' def build_payment_url(self, invoicestr, invoiceamount, invoiceid, returnurl=None): param = self.PAYPAL_COMMON param.update({ 'business': self.config('email'), 'cmd': '_xclick', 'item_name': invoicestr.encode('utf-8'), 'amount': '%.2f' % invoiceamount, 'invoice': invoiceid, 'return': '%s/p/paypal_return/%s/' % (settings.SITEBASE, self.id), }) if returnurl: # If the user cancels, send back to specific URL, instead of # the invoice url. param['cancel_return'] = returnurl return "%s?%s" % ( self.get_baseurl(), urlencode(param)) _re_paypal = re.compile('^Paypal id ([A-Z0-9]+), ') def _find_invoice_transaction(self, invoice): m = self._re_paypal.match(invoice.paymentdetails) if m: try: return (TransactionInfo.objects.get(paypaltransid=m.groups(1)[0]), None) except TransactionInfo.DoesNotExist: return (None, "not found") else: return (None, "unknown format") def payment_fees(self, invoice): (trans, reason) = self._find_invoice_transaction(invoice) if not trans: return reason return trans.fee def autorefund(self, refund): (trans, reason) = self._find_invoice_transaction(refund.invoice) if not trans: raise Exception(reason) api = PaypalAPI(self) refund.payment_reference = api.refund_transaction( trans.paypaltransid, refund.fullamount, refund.fullamount == refund.invoice.total_amount, '{0} refund {1}'.format(settings.ORG_SHORTNAME, refund.id), ) # At this point, we succeeded. Anything that failed will bubble # up as an exception. return True def used_method_details(self, invoice): # Bank transfers don't need any extra information return "PayPal"