From 26ee8bdfabcef007d978061805bebb1a3ac63157 Mon Sep 17 00:00:00 2001 From: Marc Date: Wed, 30 Jul 2014 12:55:33 +0000 Subject: [PATCH] Improvements on bank transfer payment method --- TODO.md | 3 +- orchestra/admin/menu.py | 4 +- orchestra/apps/payments/actions.py | 5 + orchestra/apps/payments/admin.py | 26 +- orchestra/apps/payments/forms.py | 68 +- orchestra/apps/payments/methods.py | 185 ---- orchestra/apps/payments/methods/__init__.py | 3 + .../apps/payments/methods/banktransfer.py | 266 +++++ orchestra/apps/payments/methods/creditcard.py | 30 + orchestra/apps/payments/methods/options.py | 48 + .../apps/payments/methods/pain.001.001.03.xsd | 921 ++++++++++++++++++ .../apps/payments/methods/pain.008.001.02.xsd | 879 +++++++++++++++++ orchestra/apps/payments/models.py | 8 +- .../conf/project_template/media/.gitignore | 0 14 files changed, 2211 insertions(+), 235 deletions(-) create mode 100644 orchestra/apps/payments/actions.py delete mode 100644 orchestra/apps/payments/methods.py create mode 100644 orchestra/apps/payments/methods/__init__.py create mode 100644 orchestra/apps/payments/methods/banktransfer.py create mode 100644 orchestra/apps/payments/methods/creditcard.py create mode 100644 orchestra/apps/payments/methods/options.py create mode 100644 orchestra/apps/payments/methods/pain.001.001.03.xsd create mode 100644 orchestra/apps/payments/methods/pain.008.001.02.xsd create mode 100644 orchestra/conf/project_template/media/.gitignore diff --git a/TODO.md b/TODO.md index f04b9aa2..90cc6e87 100644 --- a/TODO.md +++ b/TODO.md @@ -61,7 +61,7 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * DOCUMENT: orchestration.middleware: we need to know when an operation starts and ends in order to perform bulk server updates and also to wait for related objects to be saved (base object is saved first and then related) orders.signales: we perform changes right away because data model state can change under monitoring and other periodik task, and we should keep orders consistency under any situation. dependency collector with max_recursion that matches the number of dots on service.match and service.metric - + * Be consistent with dates: * created_on date @@ -72,3 +72,4 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon * backend logs with hal logo * Use logs for storing monitored values +* set_password orchestration method? diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index 0b4362f0..ac481170 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -62,8 +62,10 @@ def get_account_items(): if isinstalled('orchestra.apps.payments'): url = reverse('admin:payments_transaction_changelist') childrens.append(items.MenuItem(_("Transactions"), url)) + url = reverse('admin:payments_paymentprocess_changelist') + childrens.append(items.MenuItem(_("Payment processes"), url)) url = reverse('admin:payments_paymentsource_changelist') - childrens.append(items.MenuItem(_("Payment Sources"), url)) + childrens.append(items.MenuItem(_("Payment sources"), url)) if isinstalled('orchestra.apps.issues'): url = reverse('admin:issues_ticket_changelist') childrens.append(items.MenuItem(_("Tickets"), url)) diff --git a/orchestra/apps/payments/actions.py b/orchestra/apps/payments/actions.py new file mode 100644 index 00000000..83295b4d --- /dev/null +++ b/orchestra/apps/payments/actions.py @@ -0,0 +1,5 @@ +from .methods import BankTransfer + +def process_transactions(modeladmin, request, queryset): + BankTransfer().process(queryset) + diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py index faf2994b..fbff6333 100644 --- a/orchestra/apps/payments/admin.py +++ b/orchestra/apps/payments/admin.py @@ -6,7 +6,7 @@ from orchestra.admin.utils import admin_colored, admin_link from orchestra.apps.accounts.admin import AccountAdminMixin from .actions import process_transactions -from .methods import DirectDebit +from .methods import BankTransfer from .models import PaymentSource, Transaction, PaymentProcess @@ -35,7 +35,7 @@ class TransactionAdmin(admin.ModelAdmin): class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin): list_display = ('label', 'method', 'number', 'account_link', 'is_active') list_filter = ('method', 'is_active') - form = DirectDebit().get_form() + form = BankTransfer().get_form() # TODO select payment source method @@ -51,13 +51,21 @@ class PaymentProcessAdmin(admin.ModelAdmin): file_url.admin_order_field = 'file' def display_transactions(self, process): - links = [] - for transaction in process.transactions.all(): - url = reverse('admin:payments_transaction_change', args=(transaction.pk,)) - links.append( - '%s' % (url, str(transaction)) - ) - return '
'.join(links) + ids = [] + lines = [] + counter = 0 + tx_ids = process.transactions.order_by('id').values_list('id', flat=True) + for tx_id in tx_ids: + ids.append(str(tx_id)) + counter += 1 + if counter > 10: + counter = 0 + lines.append(','.join(ids)) + ids = [] + lines.append(','.join(ids)) + url = reverse('admin:payments_transaction_changelist') + url += '?processes=%i' % process.id + return '%s' % (url, '
'.join(lines)) display_transactions.short_description = _("Transactions") display_transactions.allow_tags = True diff --git a/orchestra/apps/payments/forms.py b/orchestra/apps/payments/forms.py index 47fdc3b0..a9353649 100644 --- a/orchestra/apps/payments/forms.py +++ b/orchestra/apps/payments/forms.py @@ -1,46 +1,40 @@ from django import forms from django.utils.translation import ugettext_lazy as _ -from django_iban.forms import IBANFormField -class PaymentSourceDataForm(forms.ModelForm): - class Meta: - exclude = ('data', 'method') +# TODO this is for the billing phase +class TransactionCreationForm(forms.ModelForm): +# transaction_link = forms.CharField() +# account_link = forms.CharField() +# bill_link = forms.CharField() + source = forms.ChoiceField(required=False) +# exclude = forms.BooleanField(required=False) + +# class Meta: +# model = Bill ? def __init__(self, *args, **kwargs): - super(PaymentSourceDataForm, self).__init__(*args, **kwargs) - instance = kwargs.get('instance') - if instance: - for field in self.declared_fields: - initial = self.fields[field].initial - self.fields[field].initial = instance.data.get(field, initial) + super(SourceSelectionForm, self).__init__(*args, **kwargs) + bill = kwargs.get('instance') + if bill: + sources = bill.account.payment_sources.filter(is_active=True) + choices = [] + for source in sources: + if bill.ammount < 0: + if source.method_class().allow_recharge: + choices.append((source.method, source.method_display())) + else: + choices.append((source.method, source.method_display())) + self.fields['source'].choices = choices - def save(self, commit=True): - plugin = self.plugin - self.instance.method = plugin.get_plugin_name() - self.instance.data = { - field: self.cleaned_data[field] for field in self.declared_fields - } - return super(PaymentSourceDataForm, self).save(commit=commit) +# def clean(self): +# cleaned_data = super(SourceSelectionForm, self).clean() +# method = cleaned_data.get("method") +# exclude = cleaned_data.get("exclude") +# if not method and not exclude: +# raise forms.ValidationError(_("A transaction should be explicitly " +# "excluded when no payment source is available.")) -class DirectDebitForm(PaymentSourceDataForm): - iban = IBANFormField(label='IBAN', - widget=forms.TextInput(attrs={'size': '50'})) - name = forms.CharField(max_length=128, label=_("Name"), - widget=forms.TextInput(attrs={'size': '50'})) - - -class CreditCardForm(PaymentSourceDataForm): - label = forms.CharField(max_length=128, label=_("Label"), - help_text=_("Use a name such as \"Jo's Visa\" to remember which " - "card it is.")) - first_name = forms.CharField(max_length=128) - last_name = forms.CharField(max_length=128) - address = forms.CharField(max_length=128) - zip = forms.CharField(max_length=128) - city = forms.CharField(max_length=128) - country = forms.CharField(max_length=128) - card_number = forms.CharField(max_length=128) - expiration_date = forms.CharField(max_length=128) - security_code = forms.CharField(max_length=128) +class ProcessTransactionForm(forms.ModelForm): + pass diff --git a/orchestra/apps/payments/methods.py b/orchestra/apps/payments/methods.py deleted file mode 100644 index cf3b6231..00000000 --- a/orchestra/apps/payments/methods.py +++ /dev/null @@ -1,185 +0,0 @@ -import os -import lxml.builder -from lxml import etree -from lxml.builder import E -from StringIO import StringIO - -from django.conf import settings as djsettings -from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ -from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH -from rest_framework import serializers - -from orchestra.utils import plugins - -from . import settings -from .forms import DirectDebitForm, CreditCardForm - - -class PaymentMethod(plugins.Plugin): - label_field = 'label' - number_field = 'number' - - __metaclass__ = plugins.PluginMount - - def get_form(self): - self.form.plugin = self - return self.form - - def get_serializer(self): - self.serializer.plugin = self - return self.serializer - - def get_label(self, data): - return data[self.label_field] - - def get_number(self, data): - return data[self.number_field] - - -class DirectDebitSerializer(serializers.Serializer): - iban = serializers.CharField(label='IBAN', validators=[IBANValidator()], - min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34) - name = serializers.CharField(label=_("Name"), max_length=128) - - -class CreditCardSerializer(serializers.Serializer): - pass - - -class DirectDebit(PaymentMethod): - verbose_name = _("Direct debit") - label_field = 'name' - number_field = 'iban' - form = DirectDebitForm - serializer = DirectDebitSerializer - - def _process_transactions(self, transactions): - for transaction in transactions: - self.object.transactions.add(transaction) - # TODO transaction.account - account = transaction.bill.account - # FIXME - data = account.payment_sources.first().data - transaction.state = transaction.WAITTING_CONFIRMATION - transaction.save() - yield E.DrctDbtTxInf( # Direct Debit Transaction Info - E.PmtId( # Payment Id - E.EndToEndId(str(transaction.id)) # Payment Id/End to End - ), - E.InstdAmt( # Instructed Amount - str(transaction.amount), - Ccy=transaction.currency.upper() - ), - E.DrctDbtTx( # Direct Debit Transaction - E.MndtRltdInf( # Mandate Related Info - E.MndtId(str(account.id)), # Mandate Id - E.DtOfSgntr( # Date of Signature - account.register_date.strftime("%Y-%m-%d") - ) - ) - ), - E.DbtrAgt( # Debtor Agent - E.FinInstnId( # Financial Institution Id - E.Othr( - E.Id('NOTPROVIDED') - ) - ) - ), - E.Dbtr( # Debtor - E.Nm(account.name), # Name - ), - E.DbtrAcct( # Debtor Account - E.Id( - E.IBAN(data['iban']) - ), - ), - ) - - def process(self, transactions): - from .models import PaymentProcess - self.object = PaymentProcess.objects.create() - creditor_name = settings.PAYMENTS_DD_CREDITOR_NAME - creditor_iban = settings.PAYMENTS_DD_CREDITOR_IBAN - creditor_bic = settings.PAYMENTS_DD_CREDITOR_BIC - creditor_at02_id = settings.PAYMENTS_DD_CREDITOR_AT02_ID - now = timezone.now() - total = str(sum([transaction.amount for transaction in transactions])) - sepa = lxml.builder.ElementMaker( - nsmap = { - 'xsi': "http://www.w3.org/2001/XMLSchema-instance", - None: "urn:iso:std:iso:20022:tech:xsd:pain.008.001.02", - } - ) - sepa = sepa.Document( - E.CstmrDrctDbtInitn( - E.GrpHdr( # Group Header - E.MsgId(str(self.object.id)), # Message Id - E.CreDtTm(now.strftime("%Y-%m-%dT%H:%M:%S")), # Creation Date Time - E.NbOfTxs(str(len(transactions))), # Number of Transactions - E.CtrlSum(total), # Control Sum - E.InitgPty( # Initiating Party - E.Nm(creditor_name), # Name - E.Id( # Identification - E.OrgId( # Organisation Id - E.Othr( - E.Id(creditor_at02_id) - ) - ) - ) - ) - ), - E.PmtInf( # Payment Info - E.PmtInfId(str(self.object.id)), # Payment Id - E.PmtMtd("DD"), # Payment Method - E.NbOfTxs(str(len(transactions))), # Number of Transactions - E.CtrlSum(total), # Control Sum - E.PmtTpInf( # Payment Type Info - E.SvcLvl( # Service Level - E.Cd("SEPA") # Code - ), - E.LclInstrm( # Local Instrument - E.Cd("CORE") # Code - ), - E.SeqTp("RCUR") # Sequence Type - ), - E.ReqdColltnDt(now.strftime("%Y-%m-%d")), # Requested Collection Date - E.Cdtr( # Creditor - E.Nm(creditor_name) - ), - E.CdtrAcct( # Creditor Account - E.Id( - E.IBAN(creditor_iban) - ) - ), - E.CdtrAgt( # Creditor Agent - E.FinInstnId( # Financial Institution Id - E.BIC(creditor_bic) - ) - ), - *list(self._process_transactions(transactions)) # Transactions - ) - ) - ) - # http://www.iso20022.org/documents/messages/1_0_version/pain/schemas/pain.008.001.02.zip - path = os.path.dirname(os.path.realpath(__file__)) - xsd_path = os.path.join(path, 'pain.008.001.02.xsd') - schema_doc = etree.parse(xsd_path) - schema = etree.XMLSchema(schema_doc) - sepa = etree.parse(StringIO(etree.tostring(sepa))) - schema.assertValid(sepa) - base_path = self.object.file.field.upload_to or djsettings.MEDIA_ROOT - file_name = 'payment-process-%i.xml' % self.object.id - file_path = os.path.join(base_path, file_name) - sepa.write(file_path, - pretty_print=True, - xml_declaration=True, - encoding='UTF-8') - self.object.file = file_name - self.object.save() - - -class CreditCard(PaymentMethod): - verbose_name = _("Credit card") - form = CreditCardForm - serializer = CreditCardSerializer diff --git a/orchestra/apps/payments/methods/__init__.py b/orchestra/apps/payments/methods/__init__.py new file mode 100644 index 00000000..589e72a0 --- /dev/null +++ b/orchestra/apps/payments/methods/__init__.py @@ -0,0 +1,3 @@ +from .creditcard import CreditCard +from .banktransfer import BankTransfer +from .options import PaymentMethod, PaymentSourceDataForm diff --git a/orchestra/apps/payments/methods/banktransfer.py b/orchestra/apps/payments/methods/banktransfer.py new file mode 100644 index 00000000..13392c83 --- /dev/null +++ b/orchestra/apps/payments/methods/banktransfer.py @@ -0,0 +1,266 @@ +import os +import lxml.builder +from lxml import etree +from lxml.builder import E +from StringIO import StringIO + +from django import forms +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ +from django_iban.forms import IBANFormField +from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH +from rest_framework import serializers + +from .. import settings +from .options import PaymentSourceDataForm, PaymentMethod + + +class BankTransferForm(PaymentSourceDataForm): + iban = IBANFormField(label='IBAN', + widget=forms.TextInput(attrs={'size': '50'})) + name = forms.CharField(max_length=128, label=_("Name"), + widget=forms.TextInput(attrs={'size': '50'})) + + +class BankTransferSerializer(serializers.Serializer): + iban = serializers.CharField(label='IBAN', validators=[IBANValidator()], + min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34) + name = serializers.CharField(label=_("Name"), max_length=128) + + +class BankTransfer(PaymentMethod): + verbose_name = _("Bank transfer") + label_field = 'name' + number_field = 'iban' + process_credit = True + form = BankTransferForm + serializer = BankTransferSerializer + + def process(self, transactions): + debts = [] + credits = [] + for transaction in transactions: + if transaction.amount < 0: + credits.append(transaction) + else: + debts.append(transaction) + if debts: + self._process_debts(debts) + if credits: + self._process_credits(credits) + + def _process_credits(self, transactions): + from ..models import PaymentProcess + self.object = PaymentProcess.objects.create() + context = self.get_context(transactions) + sepa = lxml.builder.ElementMaker( + nsmap = { + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + None: 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.03', + } + ) + sepa = sepa.Document( + E.CstmrCdtTrfInitn( + self._get_header(context), + E.PmtInf( # Payment Info + E.PmtInfId(str(self.object.id)), # Payment Id + E.PmtMtd("TRF"), # Payment Method + E.NbOfTxs(context['num_transactions']), # Number of Transactions + E.CtrlSum(context['total']), # Control Sum + E.ReqdExctnDt ( # Requested Execution Date + context['now'].strftime("%Y-%m-%d") + ), + E.Dbtr( # Debtor + E.Nm(context['name']) + ), + E.DbtrAcct( # Debtor Account + E.Id( + E.IBAN(context['iban']) + ) + ), + E.DbtrAgt( # Debtor Agent + E.FinInstnId( # Financial Institution Id + E.BIC(context['bic']) + ) + ), + *list(self._get_credit_transactions(transactions)) # Transactions + ) + ) + ) + file_name = 'credit-transfer-%i.xml' % self.object.id + self._process_xml(sepa, 'pain.001.001.03.xsd', file_name) + + def _process_debts(self, transactions): + from ..models import PaymentProcess + self.object = PaymentProcess.objects.create() + context = self.get_context(transactions) + sepa = lxml.builder.ElementMaker( + nsmap = { + 'xsi': 'http://www.w3.org/2001/XMLSchema-instance', + None: 'urn:iso:std:iso:20022:tech:xsd:pain.008.001.02', + } + ) + sepa = sepa.Document( + E.CstmrDrctDbtInitn( + self._get_header(context), + E.PmtInf( # Payment Info + E.PmtInfId(str(self.object.id)), # Payment Id + E.PmtMtd("DD"), # Payment Method + E.NbOfTxs(context['num_transactions']), # Number of Transactions + E.CtrlSum(context['total']), # Control Sum + E.PmtTpInf( # Payment Type Info + E.SvcLvl( # Service Level + E.Cd("SEPA") # Code + ), + E.LclInstrm( # Local Instrument + E.Cd("CORE") # Code + ), + E.SeqTp("RCUR") # Sequence Type + ), + E.ReqdColltnDt( # Requested Collection Date + context['now'].strftime("%Y-%m-%d") + ), + E.Cdtr( # Creditor + E.Nm(context['name']) + ), + E.CdtrAcct( # Creditor Account + E.Id( + E.IBAN(context['iban']) + ) + ), + E.CdtrAgt( # Creditor Agent + E.FinInstnId( # Financial Institution Id + E.BIC(context['bic']) + ) + ), + *list(self._get_debt_transactions(transactions)) # Transactions + ) + ) + ) + file_name = 'direct-debit-%i.xml' % self.object.id + self._process_xml(sepa, 'pain.008.001.02.xsd', file_name) + + def get_context(self, transactions): + return { + 'name': settings.PAYMENTS_DD_CREDITOR_NAME, + 'iban': settings.PAYMENTS_DD_CREDITOR_IBAN, + 'bic': settings.PAYMENTS_DD_CREDITOR_BIC, + 'at02_id': settings.PAYMENTS_DD_CREDITOR_AT02_ID, + 'now': timezone.now(), + 'total': str(sum([abs(transaction.amount) for transaction in transactions])), + 'num_transactions': str(len(transactions)), + } + + def _get_debt_transactions(self, transactions): + for transaction in transactions: + self.object.transactions.add(transaction) + # TODO transaction.account + account = transaction.bill.account + # FIXME + data = account.payment_sources.first().data + transaction.state = transaction.WAITTING_CONFIRMATION + transaction.save() + yield E.DrctDbtTxInf( # Direct Debit Transaction Info + E.PmtId( # Payment Id + E.EndToEndId(str(transaction.id)) # Payment Id/End to End + ), + E.InstdAmt( # Instructed Amount + str(abs(transaction.amount)), + Ccy=transaction.currency.upper() + ), + E.DrctDbtTx( # Direct Debit Transaction + E.MndtRltdInf( # Mandate Related Info + E.MndtId(str(account.id)), # Mandate Id + E.DtOfSgntr( # Date of Signature + account.register_date.strftime("%Y-%m-%d") + ) + ) + ), + E.DbtrAgt( # Debtor Agent + E.FinInstnId( # Financial Institution Id + E.Othr( + E.Id('NOTPROVIDED') + ) + ) + ), + E.Dbtr( # Debtor + E.Nm(account.name), # Name + ), + E.DbtrAcct( # Debtor Account + E.Id( + E.IBAN(data['iban']) + ), + ), + ) + + def _get_credit_transactions(self, transactions): + for transaction in transactions: + self.object.transactions.add(transaction) + # TODO transaction.account + account = transaction.bill.account + # FIXME + data = account.payment_sources.first().data + transaction.state = transaction.WAITTING_CONFIRMATION + transaction.save() + yield E.CdtTrfTxInf( # Credit Transfer Transaction Info + E.PmtId( # Payment Id + E.EndToEndId(str(transaction.id)) # Payment Id/End to End + ), + E.Amt( # Amount + E.InstdAmt( # Instructed Amount + str(abs(transaction.amount)), + Ccy=transaction.currency.upper() + ) + ), + E.CdtrAgt( # Creditor Agent + E.FinInstnId( # Financial Institution Id + E.Othr( + E.Id('NOTPROVIDED') + ) + ) + ), + E.Cdtr( # Debtor + E.Nm(account.name), # Name + ), + E.CdtrAcct( # Creditor Account + E.Id( + E.IBAN(data['iban']) + ), + ), + ) + + def _get_header(self, context): + return E.GrpHdr( # Group Header + E.MsgId(str(self.object.id)), # Message Id + E.CreDtTm( # Creation Date Time + context['now'].strftime("%Y-%m-%dT%H:%M:%S") + ), + E.NbOfTxs(context['num_transactions']), # Number of Transactions + E.CtrlSum(context['total']), # Control Sum + E.InitgPty( # Initiating Party + E.Nm(context['name']), # Name + E.Id( # Identification + E.OrgId( # Organisation Id + E.Othr( + E.Id(context['at02_id']) + ) + ) + ) + ) + ) + + def _process_xml(self, sepa, xsd, file_name): + # http://www.iso20022.org/documents/messages/1_0_version/pain/schemas/pain.008.001.02.zip + path = os.path.dirname(os.path.realpath(__file__)) + xsd_path = os.path.join(path, xsd) + schema_doc = etree.parse(xsd_path) + schema = etree.XMLSchema(schema_doc) + sepa = etree.parse(StringIO(etree.tostring(sepa))) + schema.assertValid(sepa) + self.object.file = file_name + self.object.save() + sepa.write(self.object.file.path, + pretty_print=True, + xml_declaration=True, + encoding='UTF-8') + diff --git a/orchestra/apps/payments/methods/creditcard.py b/orchestra/apps/payments/methods/creditcard.py new file mode 100644 index 00000000..89de5ffd --- /dev/null +++ b/orchestra/apps/payments/methods/creditcard.py @@ -0,0 +1,30 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from .options import PaymentSourceDataForm, PaymentMethod + + +class CreditCardForm(PaymentSourceDataForm): + label = forms.CharField(max_length=128, label=_("Label"), + help_text=_("Use a name such as \"Jo's Visa\" to remember which " + "card it is.")) + first_name = forms.CharField(max_length=128) + last_name = forms.CharField(max_length=128) + address = forms.CharField(max_length=128) + zip = forms.CharField(max_length=128) + city = forms.CharField(max_length=128) + country = forms.CharField(max_length=128) + card_number = forms.CharField(max_length=128) + expiration_date = forms.CharField(max_length=128) + security_code = forms.CharField(max_length=128) + + +class CreditCardSerializer(serializers.Serializer): + pass + + +class CreditCard(PaymentMethod): + verbose_name = _("Credit card") + form = CreditCardForm + serializer = CreditCardSerializer diff --git a/orchestra/apps/payments/methods/options.py b/orchestra/apps/payments/methods/options.py new file mode 100644 index 00000000..11dd302c --- /dev/null +++ b/orchestra/apps/payments/methods/options.py @@ -0,0 +1,48 @@ +from django import forms + +from orchestra.utils import plugins + + +class PaymentMethod(plugins.Plugin): + label_field = 'label' + number_field = 'number' + process_credit = False + form = None + serializer = None + + __metaclass__ = plugins.PluginMount + + def get_form(self): + self.form.plugin = self + return self.form + + def get_serializer(self): + self.serializer.plugin = self + return self.serializer + + def get_label(self, data): + return data[self.label_field] + + def get_number(self, data): + return data[self.number_field] + + +class PaymentSourceDataForm(forms.ModelForm): + class Meta: + exclude = ('data',) # TODO add 'method' + + def __init__(self, *args, **kwargs): + super(PaymentSourceDataForm, self).__init__(*args, **kwargs) + instance = kwargs.get('instance') + if instance: + for field in self.declared_fields: + initial = self.fields[field].initial + self.fields[field].initial = instance.data.get(field, initial) + + def save(self, commit=True): + plugin = self.plugin + self.instance.method = plugin.get_plugin_name() + self.instance.data = { + field: self.cleaned_data[field] for field in self.declared_fields + } + return super(PaymentSourceDataForm, self).save(commit=commit) diff --git a/orchestra/apps/payments/methods/pain.001.001.03.xsd b/orchestra/apps/payments/methods/pain.001.001.03.xsd new file mode 100644 index 00000000..4f65ddcc --- /dev/null +++ b/orchestra/apps/payments/methods/pain.001.001.03.xsd @@ -0,0 +1,921 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/apps/payments/methods/pain.008.001.02.xsd b/orchestra/apps/payments/methods/pain.008.001.02.xsd new file mode 100644 index 00000000..394b8045 --- /dev/null +++ b/orchestra/apps/payments/methods/pain.008.001.02.xsd @@ -0,0 +1,879 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py index eb104617..bcc266f4 100644 --- a/orchestra/apps/payments/models.py +++ b/orchestra/apps/payments/models.py @@ -38,6 +38,7 @@ class PaymentSource(models.Model): return plugin.get_number(self.data) +# TODO lock transaction in waiting confirmation class Transaction(models.Model): WAITTING_PROCESSING = 'WAITTING_PROCESSING' WAITTING_CONFIRMATION = 'WAITTING_CONFIRMATION' @@ -57,8 +58,8 @@ class Transaction(models.Model): # TODO account fk? bill = models.ForeignKey('bills.bill', verbose_name=_("bill"), related_name='transactions') - source = models.ForeignKey(PaymentSource, verbose_name=_("source"), - related_name='transactions') + source = models.ForeignKey(PaymentSource, null=True, blank=True, + verbose_name=_("source"), related_name='transactions') state = models.CharField(_("state"), max_length=32, choices=STATES, default=WAITTING_PROCESSING) amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2) @@ -71,6 +72,7 @@ class Transaction(models.Model): return "Transaction {}".format(self.id) +# TODO rename to TransactionProcess class PaymentProcess(models.Model): """ Stores arbitrary data generated by payment methods while processing transactions @@ -81,6 +83,8 @@ class PaymentProcess(models.Model): file = models.FileField(_("file"), blank=True) created_at = models.DateTimeField(_("created at"), auto_now_add=True) + # TODO state: created, commited, secured (delayed persistence) + def __unicode__(self): return str(self.id) diff --git a/orchestra/conf/project_template/media/.gitignore b/orchestra/conf/project_template/media/.gitignore new file mode 100644 index 00000000..e69de29b