Fixes on DD payment method
This commit is contained in:
parent
7a392df70d
commit
6902f9e559
|
@ -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.db import models
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import services
|
from orchestra.core import services
|
||||||
|
@ -8,9 +9,10 @@ from . import settings
|
||||||
|
|
||||||
|
|
||||||
class Account(models.Model):
|
class Account(models.Model):
|
||||||
user = models.OneToOneField(django_settings.AUTH_USER_MODEL, related_name='accounts')
|
user = models.OneToOneField(djsettings.AUTH_USER_MODEL,
|
||||||
type = models.CharField(_("type"), max_length=32, choices=settings.ACCOUNTS_TYPES,
|
verbose_name=_("user"), related_name='accounts')
|
||||||
default=settings.ACCOUNTS_DEFAULT_TYPE)
|
type = models.CharField(_("type"), choices=settings.ACCOUNTS_TYPES,
|
||||||
|
max_length=32, default=settings.ACCOUNTS_DEFAULT_TYPE)
|
||||||
language = models.CharField(_("language"), max_length=2,
|
language = models.CharField(_("language"), max_length=2,
|
||||||
choices=settings.ACCOUNTS_LANGUAGES,
|
choices=settings.ACCOUNTS_LANGUAGES,
|
||||||
default=settings.ACCOUNTS_DEFAULT_LANGUAGE)
|
default=settings.ACCOUNTS_DEFAULT_LANGUAGE)
|
||||||
|
@ -21,10 +23,9 @@ class Account(models.Model):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@cached_property
|
||||||
def name(self):
|
def name(self):
|
||||||
self._cached_name = getattr(self, '_cached_name', self.user.username)
|
return self.user.username
|
||||||
return self._cached_name
|
|
||||||
|
|
||||||
|
|
||||||
services.register(Account, menu=False)
|
services.register(Account, menu=False)
|
||||||
|
|
|
@ -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 import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -56,10 +56,10 @@ class Ticket(models.Model):
|
||||||
(CLOSED, 'Closed'),
|
(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)
|
related_name='tickets_created', null=True)
|
||||||
creator_name = models.CharField(_("creator name"), max_length=256, blank=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"))
|
related_name='tickets_owned', verbose_name=_("assigned to"))
|
||||||
queue = models.ForeignKey(Queue, related_name='tickets', null=True, blank=True)
|
queue = models.ForeignKey(Queue, related_name='tickets', null=True, blank=True)
|
||||||
subject = models.CharField(_("subject"), max_length=256)
|
subject = models.CharField(_("subject"), max_length=256)
|
||||||
|
@ -153,7 +153,7 @@ class Ticket(models.Model):
|
||||||
class Message(models.Model):
|
class Message(models.Model):
|
||||||
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"),
|
ticket = models.ForeignKey('issues.Ticket', verbose_name=_("ticket"),
|
||||||
related_name='messages')
|
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')
|
related_name='ticket_messages')
|
||||||
author_name = models.CharField(_("author name"), max_length=256, blank=True)
|
author_name = models.CharField(_("author name"), max_length=256, blank=True)
|
||||||
content = models.TextField(_("content"))
|
content = models.TextField(_("content"))
|
||||||
|
@ -183,7 +183,7 @@ class TicketTracker(models.Model):
|
||||||
""" Keeps track of user read tickets """
|
""" Keeps track of user read tickets """
|
||||||
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"),
|
ticket = models.ForeignKey(Ticket, verbose_name=_("ticket"),
|
||||||
related_name='trackers')
|
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')
|
related_name='ticket_trackers')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
from django.contrib import admin
|
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.admin.utils import admin_colored, admin_link
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||||
|
|
||||||
|
from .actions import process_transactions
|
||||||
from .methods import BankTransfer
|
from .methods import BankTransfer
|
||||||
from .models import PaymentSource, Transaction
|
from .models import PaymentSource, Transaction, PaymentProcess
|
||||||
|
|
||||||
|
|
||||||
STATE_COLORS = {
|
STATE_COLORS = {
|
||||||
|
@ -22,6 +25,7 @@ class TransactionAdmin(admin.ModelAdmin):
|
||||||
'id', 'bill_link', 'account_link', 'source', 'display_state', 'amount'
|
'id', 'bill_link', 'account_link', 'source', 'display_state', 'amount'
|
||||||
)
|
)
|
||||||
list_filter = ('source__method', 'state')
|
list_filter = ('source__method', 'state')
|
||||||
|
actions = (process_transactions,)
|
||||||
|
|
||||||
bill_link = admin_link('bill')
|
bill_link = admin_link('bill')
|
||||||
account_link = admin_link('bill__account')
|
account_link = admin_link('bill__account')
|
||||||
|
@ -35,5 +39,29 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
# TODO select payment source method
|
# 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(PaymentSource, PaymentSourceAdmin)
|
||||||
admin.site.register(Transaction, TransactionAdmin)
|
admin.site.register(Transaction, TransactionAdmin)
|
||||||
|
admin.site.register(PaymentProcess, PaymentProcessAdmin)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import random
|
import os
|
||||||
import string
|
import lxml.builder
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from lxml.builder import E
|
from lxml.builder import E
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
from django.conf import settings as djsettings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH
|
from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH
|
||||||
|
@ -52,28 +54,28 @@ class BankTransfer(PaymentMethod):
|
||||||
form = BankTransferForm
|
form = BankTransferForm
|
||||||
serializer = BankTransferSerializer
|
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):
|
def _process_transactions(self, transactions):
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
account = transaction.account
|
self.object.transactions.add(transaction)
|
||||||
data = transaction.data
|
# TODO transaction.account
|
||||||
transaction.info = self.payment_id
|
account = transaction.bill.account
|
||||||
|
# FIXME
|
||||||
|
data = account.payment_sources.first().data
|
||||||
transaction.state = transaction.WAITTING_CONFIRMATION
|
transaction.state = transaction.WAITTING_CONFIRMATION
|
||||||
transaction.save()
|
transaction.save()
|
||||||
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
|
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
|
||||||
E.PmtId( # Payment Id
|
E.PmtId( # Payment Id
|
||||||
E.EndToEndId(str(transaction.id)) # Payment Id/End to End
|
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.DrctDbtTx( # Direct Debit Transaction
|
||||||
E.MndtRltdInf( # Mandate Related Info
|
E.MndtRltdInf( # Mandate Related Info
|
||||||
E.MndtId(str(account.id)), # Mandate Id
|
E.MndtId(str(account.id)), # Mandate Id
|
||||||
E.DtOfSgntr( # Date of Signature
|
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):
|
def process(self, transactions):
|
||||||
self.set_id()
|
from .models import PaymentProcess
|
||||||
|
self.object = PaymentProcess.objects.create()
|
||||||
creditor_name = settings.PAYMENTS_DD_CREDITOR_NAME
|
creditor_name = settings.PAYMENTS_DD_CREDITOR_NAME
|
||||||
creditor_iban = settings.PAYMENTS_DD_CREDITOR_IBAN
|
creditor_iban = settings.PAYMENTS_DD_CREDITOR_IBAN
|
||||||
creditor_bic = settings.PAYMENTS_DD_CREDITOR_BIC
|
creditor_bic = settings.PAYMENTS_DD_CREDITOR_BIC
|
||||||
creditor_at02_id = settings.PAYMENTS_DD_CREDITOR_AT02_ID
|
creditor_at02_id = settings.PAYMENTS_DD_CREDITOR_AT02_ID
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
total = str(sum([transaction.amount for transaction in transactions]))
|
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.CstmrDrctDbtInitn(
|
||||||
E.GrpHdr( # Group Header
|
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.CreDtTm(now.strftime("%Y-%m-%dT%H:%M:%S")), # Creation Date Time
|
||||||
E.NbOfTxs(str(len(transactions))), # Number of Transactions
|
E.NbOfTxs(str(len(transactions))), # Number of Transactions
|
||||||
E.CtrlSum(total), # Control Sum
|
E.CtrlSum(total), # Control Sum
|
||||||
|
@ -114,14 +123,14 @@ class BankTransfer(PaymentMethod):
|
||||||
E.Id( # Identification
|
E.Id( # Identification
|
||||||
E.OrgId( # Organisation Id
|
E.OrgId( # Organisation Id
|
||||||
E.Othr(
|
E.Othr(
|
||||||
E.Id(creditor_at_02)
|
E.Id(creditor_at02_id)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
E.PmtInf( # Payment Info
|
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.PmtMtd("DD"), # Payment Method
|
||||||
E.NbOfTxs(str(len(transactions))), # Number of Transactions
|
E.NbOfTxs(str(len(transactions))), # Number of Transactions
|
||||||
E.CtrlSum(total), # Control Sum
|
E.CtrlSum(total), # Control Sum
|
||||||
|
@ -134,7 +143,7 @@ class BankTransfer(PaymentMethod):
|
||||||
),
|
),
|
||||||
E.SeqTp("RCUR") # Sequence Type
|
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.Cdtr( # Creditor
|
||||||
E.Nm(creditor_name)
|
E.Nm(creditor_name)
|
||||||
),
|
),
|
||||||
|
@ -150,19 +159,24 @@ class BankTransfer(PaymentMethod):
|
||||||
),
|
),
|
||||||
*list(self._process_transactions(transactions)) # Transactions
|
*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
|
# 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)
|
schema.assertValid(sepa)
|
||||||
# TODO where to save this shit?
|
base_path = self.object.file.field.upload_to or djsettings.MEDIA_ROOT
|
||||||
# TODO new model? Payment with batch support, How this relates to transaction?
|
file_name = 'payment-process-%i.xml' % self.object.id
|
||||||
# TODO positive only amount ?
|
file_path = os.path.join(base_path, file_name)
|
||||||
# TODO what with negative amounts? what are amendments?
|
sepa.write(file_path,
|
||||||
return etree.tostring(page, pretty_print=True, xml_declaration=True)
|
pretty_print=True,
|
||||||
|
xml_declaration=True,
|
||||||
|
encoding='UTF-8')
|
||||||
|
self.object.file = file_name
|
||||||
|
self.object.save()
|
||||||
|
|
||||||
|
|
||||||
class CreditCard(PaymentMethod):
|
class CreditCard(PaymentMethod):
|
||||||
|
|
|
@ -61,7 +61,6 @@ class Transaction(models.Model):
|
||||||
related_name='transactions')
|
related_name='transactions')
|
||||||
state = models.CharField(_("state"), max_length=32, choices=STATES,
|
state = models.CharField(_("state"), max_length=32, choices=STATES,
|
||||||
default=WAITTING_PROCESSING)
|
default=WAITTING_PROCESSING)
|
||||||
data = JSONField(_("data"))
|
|
||||||
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
||||||
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
|
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
|
||||||
created_on = models.DateTimeField(auto_now_add=True)
|
created_on = models.DateTimeField(auto_now_add=True)
|
||||||
|
@ -72,5 +71,19 @@ class Transaction(models.Model):
|
||||||
return "Transaction {}".format(self.id)
|
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(PaymentSource)
|
||||||
accounts.register(Transaction)
|
accounts.register(Transaction)
|
||||||
|
|
|
@ -7,8 +7,8 @@ PAYMENT_CURRENCY = getattr(settings, 'PAYMENT_CURRENCY', 'Eur')
|
||||||
PAYMENTS_DD_CREDITOR_NAME = getattr(settings, 'PAYMENTS_DD_CREDITOR_NAME',
|
PAYMENTS_DD_CREDITOR_NAME = getattr(settings, 'PAYMENTS_DD_CREDITOR_NAME',
|
||||||
'Orchestra')
|
'Orchestra')
|
||||||
PAYMENTS_DD_CREDITOR_IBAN = getattr(settings, 'PAYMENTS_DD_CREDITOR_IBAN',
|
PAYMENTS_DD_CREDITOR_IBAN = getattr(settings, 'PAYMENTS_DD_CREDITOR_IBAN',
|
||||||
'InvalidIBAN')
|
'IE98BOFI90393912121212')
|
||||||
PAYMENTS_DD_CREDITOR_BIC = getattr(settings, 'PAYMENTS_DD_CREDITOR_BIC',
|
PAYMENTS_DD_CREDITOR_BIC = getattr(settings, 'PAYMENTS_DD_CREDITOR_BIC',
|
||||||
'InvalidBIC')
|
'BOFIIE2D')
|
||||||
PAYMENTS_DD_CREDITOR_AT02_ID = getattr(settings, 'PAYMENTS_DD_CREDITOR_AT02_ID',
|
PAYMENTS_DD_CREDITOR_AT02_ID = getattr(settings, 'PAYMENTS_DD_CREDITOR_AT02_ID',
|
||||||
'InvalidAT02ID')
|
'InvalidAT02ID')
|
||||||
|
|
|
@ -31,6 +31,7 @@ USE_TZ = True
|
||||||
# Example: "http://media.lawrence.com/static/"
|
# Example: "http://media.lawrence.com/static/"
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
ALLOWED_HOSTS = '*'
|
ALLOWED_HOSTS = '*'
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,10 @@ DATABASES = {
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
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_HOST = 'smtp.yourhost.eu'
|
||||||
# EMAIL_PORT = ''
|
# EMAIL_PORT = ''
|
||||||
# EMAIL_HOST_USER = ''
|
# EMAIL_HOST_USER = ''
|
||||||
|
|
|
@ -21,6 +21,10 @@ urlpatterns = patterns('',
|
||||||
'rest_framework.authtoken.views.obtain_auth_token',
|
'rest_framework.authtoken.views.obtain_auth_token',
|
||||||
name='api-token-auth'
|
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}
|
||||||
|
)
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue