From 6902f9e5598f9c004007969e866c25c5bb974e34 Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 29 Jul 2014 20:10:37 +0000 Subject: [PATCH] Fixes on DD payment method --- orchestra/apps/accounts/models.py | 15 ++-- orchestra/apps/issues/models.py | 10 +-- orchestra/apps/payments/admin.py | 30 +++++++- orchestra/apps/payments/methods.py | 70 +++++++++++-------- orchestra/apps/payments/models.py | 15 +++- orchestra/apps/payments/settings.py | 4 +- orchestra/conf/base_settings.py | 1 + .../project_template/project_name/settings.py | 4 ++ orchestra/urls.py | 4 ++ 9 files changed, 109 insertions(+), 44 deletions(-) diff --git a/orchestra/apps/accounts/models.py b/orchestra/apps/accounts/models.py index 6eda0dfc..3a9aa0ef 100644 --- a/orchestra/apps/accounts/models.py +++ b/orchestra/apps/accounts/models.py @@ -1,5 +1,6 @@ -from django.conf import settings as django_settings +from django.conf import settings as djsettings from django.db import models +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from orchestra.core import services @@ -8,9 +9,10 @@ from . import settings class Account(models.Model): - user = models.OneToOneField(django_settings.AUTH_USER_MODEL, related_name='accounts') - type = models.CharField(_("type"), max_length=32, choices=settings.ACCOUNTS_TYPES, - default=settings.ACCOUNTS_DEFAULT_TYPE) + user = models.OneToOneField(djsettings.AUTH_USER_MODEL, + verbose_name=_("user"), related_name='accounts') + type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES, + max_length=32, default=settings.ACCOUNTS_DEFAULT_TYPE) language = models.CharField(_("language"), max_length=2, choices=settings.ACCOUNTS_LANGUAGES, default=settings.ACCOUNTS_DEFAULT_LANGUAGE) @@ -21,10 +23,9 @@ class Account(models.Model): def __unicode__(self): return self.name - @property + @cached_property def name(self): - self._cached_name = getattr(self, '_cached_name', self.user.username) - return self._cached_name + return self.user.username services.register(Account, menu=False) diff --git a/orchestra/apps/issues/models.py b/orchestra/apps/issues/models.py index e0a8b142..7fa3ce95 100644 --- a/orchestra/apps/issues/models.py +++ b/orchestra/apps/issues/models.py @@ -1,4 +1,4 @@ -from django.conf import settings as django_settings +from django.conf import settings as djsettings from django.db import models from django.db.models import Q from django.utils.translation import ugettext_lazy as _ @@ -56,10 +56,10 @@ class Ticket(models.Model): (CLOSED, 'Closed'), ) - creator = models.ForeignKey(django_settings.AUTH_USER_MODEL, verbose_name=_("created by"), + creator = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("created by"), related_name='tickets_created', null=True) creator_name = models.CharField(_("creator name"), max_length=256, blank=True) - owner = models.ForeignKey(django_settings.AUTH_USER_MODEL, null=True, blank=True, + owner = models.ForeignKey(djsettings.AUTH_USER_MODEL, null=True, blank=True, related_name='tickets_owned', verbose_name=_("assigned to")) queue = models.ForeignKey(Queue, related_name='tickets', null=True, blank=True) subject = models.CharField(_("subject"), max_length=256) @@ -153,7 +153,7 @@ class Ticket(models.Model): class Message(models.Model): ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"), related_name='messages') - author = models.ForeignKey(django_settings.AUTH_USER_MODEL, verbose_name=_("author"), + author = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("author"), related_name='ticket_messages') author_name = models.CharField(_("author name"), max_length=256, blank=True) content = models.TextField(_("content")) @@ -183,7 +183,7 @@ class TicketTracker(models.Model): """ Keeps track of user read tickets """ ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"), related_name='trackers') - user = models.ForeignKey(django_settings.AUTH_USER_MODEL, verbose_name=_("user"), + user = models.ForeignKey(djsettings.AUTH_USER_MODEL, verbose_name=_("user"), related_name='ticket_trackers') class Meta: diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py index 3597af3c..1c236891 100644 --- a/orchestra/apps/payments/admin.py +++ b/orchestra/apps/payments/admin.py @@ -1,10 +1,13 @@ from django.contrib import admin +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ from orchestra.admin.utils import admin_colored, admin_link from orchestra.apps.accounts.admin import AccountAdminMixin +from .actions import process_transactions from .methods import BankTransfer -from .models import PaymentSource, Transaction +from .models import PaymentSource, Transaction, PaymentProcess STATE_COLORS = { @@ -22,6 +25,7 @@ class TransactionAdmin(admin.ModelAdmin): 'id', 'bill_link', 'account_link', 'source', 'display_state', 'amount' ) list_filter = ('source__method', 'state') + actions = (process_transactions,) bill_link = admin_link('bill') account_link = admin_link('bill__account') @@ -35,5 +39,29 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin): # TODO select payment source method +class PaymentProcessAdmin(admin.ModelAdmin): + list_display = ('id', 'file_url', 'display_transactions', 'created_at') + fields = ('data', 'file_url', 'display_transactions', 'created_at') + readonly_fields = ('file_url', 'display_transactions', 'created_at') + + def file_url(self, process): + if process.file: + return '%s' % (process.file.url, process.file.name) + file_url.allow_tags = True + 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) + display_transactions.short_description = _("Transactions") + display_transactions.allow_tags = True + + admin.site.register(PaymentSource, PaymentSourceAdmin) admin.site.register(Transaction, TransactionAdmin) +admin.site.register(PaymentProcess, PaymentProcessAdmin) diff --git a/orchestra/apps/payments/methods.py b/orchestra/apps/payments/methods.py index 54252c75..d287f403 100644 --- a/orchestra/apps/payments/methods.py +++ b/orchestra/apps/payments/methods.py @@ -1,8 +1,10 @@ -import random -import string +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 @@ -52,28 +54,28 @@ class BankTransfer(PaymentMethod): form = BankTransferForm serializer = BankTransferSerializer - def set_id(self): - size=6 - chars=string.ascii_uppercase + string.digits - self.payment_id = ''.join(random.choice(chars) for _ in range(size)) - def _process_transactions(self, transactions): for transaction in transactions: - account = transaction.account - data = transaction.data - transaction.info = self.payment_id + 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(transaction.amount, Ccy="EUR"), # Instructed Amount + 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.registered_on.strfrm("%Y-%m-%d") + account.register_date.strftime("%Y-%m-%d") ) ) ), @@ -95,17 +97,24 @@ class BankTransfer(PaymentMethod): ) def process(self, transactions): - self.set_id() + 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 = E.Document( + 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(self.payment_id), # Message Id + 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 @@ -114,14 +123,14 @@ class BankTransfer(PaymentMethod): E.Id( # Identification E.OrgId( # Organisation Id E.Othr( - E.Id(creditor_at_02) + E.Id(creditor_at02_id) ) ) ) ) ), E.PmtInf( # Payment Info - E.PmtInfId(self.payment_id), # Payment Id + 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 @@ -134,7 +143,7 @@ class BankTransfer(PaymentMethod): ), E.SeqTp("RCUR") # Sequence Type ), - E.ReqdColltnDt(now.strfrm("%Y-%m-%d")), # Requested Collection Date + E.ReqdColltnDt(now.strftime("%Y-%m-%d")), # Requested Collection Date E.Cdtr( # Creditor E.Nm(creditor_name) ), @@ -150,19 +159,24 @@ class BankTransfer(PaymentMethod): ), *list(self._process_transactions(transactions)) # Transactions ) - ), { - 'xmlns': "urn:iso:std:iso:20022:tech:xsd:pain.008.001.02", - 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance" - } + ) ) # http://www.iso20022.org/documents/messages/1_0_version/pain/schemas/pain.008.001.02.zip - schema = etree.parse('pain.008.001.02.xsd') + 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) - # TODO where to save this shit? - # TODO new model? Payment with batch support, How this relates to transaction? - # TODO positive only amount ? - # TODO what with negative amounts? what are amendments? - return etree.tostring(page, pretty_print=True, xml_declaration=True) + 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): diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py index ff4fcd72..eb104617 100644 --- a/orchestra/apps/payments/models.py +++ b/orchestra/apps/payments/models.py @@ -61,7 +61,6 @@ class Transaction(models.Model): related_name='transactions') state = models.CharField(_("state"), max_length=32, choices=STATES, default=WAITTING_PROCESSING) - data = JSONField(_("data")) amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2) currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY) created_on = models.DateTimeField(auto_now_add=True) @@ -72,5 +71,19 @@ class Transaction(models.Model): return "Transaction {}".format(self.id) +class PaymentProcess(models.Model): + """ + Stores arbitrary data generated by payment methods while processing transactions + """ + transactions = models.ManyToManyField(Transaction, related_name='processes', + verbose_name=_("transactions")) + data = JSONField(_("data"), blank=True) + file = models.FileField(_("file"), blank=True) + created_at = models.DateTimeField(_("created at"), auto_now_add=True) + + def __unicode__(self): + return str(self.id) + + accounts.register(PaymentSource) accounts.register(Transaction) diff --git a/orchestra/apps/payments/settings.py b/orchestra/apps/payments/settings.py index c0f591c2..188dc646 100644 --- a/orchestra/apps/payments/settings.py +++ b/orchestra/apps/payments/settings.py @@ -7,8 +7,8 @@ PAYMENT_CURRENCY = getattr(settings, 'PAYMENT_CURRENCY', 'Eur') PAYMENTS_DD_CREDITOR_NAME = getattr(settings, 'PAYMENTS_DD_CREDITOR_NAME', 'Orchestra') PAYMENTS_DD_CREDITOR_IBAN = getattr(settings, 'PAYMENTS_DD_CREDITOR_IBAN', - 'InvalidIBAN') + 'IE98BOFI90393912121212') PAYMENTS_DD_CREDITOR_BIC = getattr(settings, 'PAYMENTS_DD_CREDITOR_BIC', - 'InvalidBIC') + 'BOFIIE2D') PAYMENTS_DD_CREDITOR_AT02_ID = getattr(settings, 'PAYMENTS_DD_CREDITOR_AT02_ID', 'InvalidAT02ID') diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index 859d6459..a214faf4 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -31,6 +31,7 @@ USE_TZ = True # Example: "http://media.lawrence.com/static/" STATIC_URL = '/static/' +MEDIA_URL = '/media/' ALLOWED_HOSTS = '*' diff --git a/orchestra/conf/project_template/project_name/settings.py b/orchestra/conf/project_template/project_name/settings.py index 6e5a04ac..1e18b9a0 100644 --- a/orchestra/conf/project_template/project_name/settings.py +++ b/orchestra/conf/project_template/project_name/settings.py @@ -51,6 +51,10 @@ DATABASES = { STATIC_ROOT = os.path.join(BASE_DIR, 'static') +# Absolute filesystem path to the directory that will hold user-uploaded files. +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + + # EMAIL_HOST = 'smtp.yourhost.eu' # EMAIL_PORT = '' # EMAIL_HOST_USER = '' diff --git a/orchestra/urls.py b/orchestra/urls.py index 0b5892d9..b8848188 100644 --- a/orchestra/urls.py +++ b/orchestra/urls.py @@ -21,6 +21,10 @@ urlpatterns = patterns('', 'rest_framework.authtoken.views.obtain_auth_token', name='api-token-auth' ), + # TODO make this private + url(r'^media/(?P.*)$', 'django.views.static.serve', + {'document_root': settings.MEDIA_ROOT, 'show_indexes': True} + ) )