Fixes on DD payment method

This commit is contained in:
Marc 2014-07-29 20:10:37 +00:00
parent 7a392df70d
commit 6902f9e559
9 changed files with 109 additions and 44 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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 '<a href="%s">%s</a>' % (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(
'<a href="%s">%s</a>' % (url, str(transaction))
)
return '<br>'.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)

View File

@ -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):

View File

@ -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)

View File

@ -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')

View File

@ -31,6 +31,7 @@ USE_TZ = True
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
ALLOWED_HOSTS = '*'

View File

@ -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 = ''

View File

@ -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<path>.*)$', 'django.views.static.serve',
{'document_root': settings.MEDIA_ROOT, 'show_indexes': True}
)
)