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}
+ )
)