Preliminar implementation of SEPA payment system
This commit is contained in:
parent
bef7af084b
commit
30cc1d9922
|
@ -2,7 +2,7 @@ from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.forms.models import BaseInlineFormSet
|
from django.forms.models import BaseInlineFormSet
|
||||||
|
|
||||||
from .utils import set_default_filter
|
from .utils import set_url_query
|
||||||
|
|
||||||
|
|
||||||
class ExtendedModelAdmin(admin.ModelAdmin):
|
class ExtendedModelAdmin(admin.ModelAdmin):
|
||||||
|
@ -55,9 +55,9 @@ class ChangeListDefaultFilter(object):
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
""" Default filter as 'my_nodes=True' """
|
""" Default filter as 'my_nodes=True' """
|
||||||
defaults = []
|
defaults = []
|
||||||
for queryarg, value in self.default_changelist_filters:
|
for key, value in self.default_changelist_filters:
|
||||||
set_default_filter(queryarg, request, value)
|
set_url_query(request, key, value)
|
||||||
defaults.append(queryarg)
|
defaults.append(key)
|
||||||
# hack response cl context in order to hook default filter awaearness into search_form.html template
|
# hack response cl context in order to hook default filter awaearness into search_form.html template
|
||||||
response = super(ChangeListDefaultFilter, self).changelist_view(request, extra_context=extra_context)
|
response = super(ChangeListDefaultFilter, self).changelist_view(request, extra_context=extra_context)
|
||||||
if hasattr(response, 'context_data') and 'cl' in response.context_data:
|
if hasattr(response, 'context_data') and 'cl' in response.context_data:
|
||||||
|
|
|
@ -63,13 +63,13 @@ def wrap_admin_view(modeladmin, view):
|
||||||
return update_wrapper(wrapper, view)
|
return update_wrapper(wrapper, view)
|
||||||
|
|
||||||
|
|
||||||
def set_default_filter(queryarg, request, value):
|
def set_url_query(request, key, value):
|
||||||
""" set default filters for changelist_view """
|
""" set default filters for changelist_view """
|
||||||
if queryarg not in request.GET:
|
if key not in request.GET:
|
||||||
request_copy = request.GET.copy()
|
request_copy = request.GET.copy()
|
||||||
if callable(value):
|
if callable(value):
|
||||||
value = value(request)
|
value = value(request)
|
||||||
request_copy[queryarg] = value
|
request_copy[key] = value
|
||||||
request.GET = request_copy
|
request.GET = request_copy
|
||||||
request.META['QUERY_STRING'] = request.GET.urlencode()
|
request.META['QUERY_STRING'] = request.GET.urlencode()
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,11 @@ from django.contrib.admin.util import unquote
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.six.moves.urllib.parse import parse_qsl
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ExtendedModelAdmin
|
from orchestra.admin import ExtendedModelAdmin
|
||||||
from orchestra.admin.utils import wrap_admin_view, admin_link
|
from orchestra.admin.utils import wrap_admin_view, admin_link, set_url_query
|
||||||
from orchestra.core import services, accounts
|
from orchestra.core import services, accounts
|
||||||
|
|
||||||
from .filters import HasMainUserListFilter
|
from .filters import HasMainUserListFilter
|
||||||
|
@ -129,6 +130,8 @@ class AccountAdminMixin(object):
|
||||||
""" Provide basic account support to ModelAdmin and AdminInline classes """
|
""" Provide basic account support to ModelAdmin and AdminInline classes """
|
||||||
readonly_fields = ('account_link',)
|
readonly_fields = ('account_link',)
|
||||||
filter_by_account_fields = []
|
filter_by_account_fields = []
|
||||||
|
change_list_template = 'admin/accounts/account/change_list.html'
|
||||||
|
change_form_template = 'admin/accounts/account/change_form.html'
|
||||||
|
|
||||||
def account_link(self, instance):
|
def account_link(self, instance):
|
||||||
account = instance.account if instance.pk else self.account
|
account = instance.account if instance.pk else self.account
|
||||||
|
@ -161,6 +164,48 @@ class AccountAdminMixin(object):
|
||||||
# Filter related object by account
|
# Filter related object by account
|
||||||
formfield.queryset = formfield.queryset.filter(account=self.account)
|
formfield.queryset = formfield.queryset.filter(account=self.account)
|
||||||
return formfield
|
return formfield
|
||||||
|
|
||||||
|
def get_account_from_preserve_filters(self, request):
|
||||||
|
preserved_filters = self.get_preserved_filters(request)
|
||||||
|
preserved_filters = dict(parse_qsl(preserved_filters))
|
||||||
|
cl_filters = preserved_filters.get('_changelist_filters')
|
||||||
|
if cl_filters:
|
||||||
|
return dict(parse_qsl(cl_filters)).get('account')
|
||||||
|
|
||||||
|
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
||||||
|
account_id = self.get_account_from_preserve_filters(request)
|
||||||
|
verb = 'change' if object_id else 'add'
|
||||||
|
if not object_id:
|
||||||
|
if account_id:
|
||||||
|
# Preselect account
|
||||||
|
set_url_query(request, 'account', account_id)
|
||||||
|
context = {
|
||||||
|
'from_account': bool(account_id),
|
||||||
|
'account': not account_id or Account.objects.get(pk=account_id),
|
||||||
|
'account_opts': Account._meta,
|
||||||
|
}
|
||||||
|
context.update(extra_context or {})
|
||||||
|
return super(AccountAdminMixin, self).changeform_view(request,
|
||||||
|
object_id=object_id, form_url=form_url, extra_context=context)
|
||||||
|
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
account_id = request.GET.get('account')
|
||||||
|
context = {
|
||||||
|
'from_account': False
|
||||||
|
}
|
||||||
|
if account_id:
|
||||||
|
opts = self.model._meta
|
||||||
|
account = Account.objects.get(pk=account_id)
|
||||||
|
context = {
|
||||||
|
'from_account': True,
|
||||||
|
'title': _("Select %s to change for %s") % (
|
||||||
|
opts.verbose_name, account.name),
|
||||||
|
'account': not account_id or Account.objects.get(pk=account_id),
|
||||||
|
'account_opts': Account._meta,
|
||||||
|
}
|
||||||
|
context.update(extra_context or {})
|
||||||
|
return super(AccountAdminMixin, self).changelist_view(request,
|
||||||
|
extra_context=context)
|
||||||
|
|
||||||
|
|
||||||
class SelectAccountAdminMixin(AccountAdminMixin):
|
class SelectAccountAdminMixin(AccountAdminMixin):
|
||||||
|
@ -196,14 +241,21 @@ class SelectAccountAdminMixin(AccountAdminMixin):
|
||||||
def add_view(self, request, form_url='', extra_context=None):
|
def add_view(self, request, form_url='', extra_context=None):
|
||||||
""" Redirects to select account view if required """
|
""" Redirects to select account view if required """
|
||||||
if request.user.is_superuser:
|
if request.user.is_superuser:
|
||||||
if 'account' in request.GET or Account.objects.count() == 1:
|
from_account_id = self.get_account_from_preserve_filters(request)
|
||||||
|
if from_account_id:
|
||||||
|
set_url_query(request, 'account', from_account_id)
|
||||||
|
account_id = request.GET.get('account')
|
||||||
|
if account_id or Account.objects.count() == 1:
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if 'account' in request.GET:
|
if account_id:
|
||||||
kwargs = dict(pk=request.GET['account'])
|
kwargs = dict(pk=account_id)
|
||||||
self.account = Account.objects.get(**kwargs)
|
self.account = Account.objects.get(**kwargs)
|
||||||
opts = self.model._meta
|
opts = self.model._meta
|
||||||
context = {
|
context = {
|
||||||
'title': _("Add %s for %s") % (opts.verbose_name, self.account.name)
|
'title': _("Add %s for %s") % (opts.verbose_name, self.account.name),
|
||||||
|
'from_account': bool(from_account_id),
|
||||||
|
'account': self.account,
|
||||||
|
'account_opts': Account._meta,
|
||||||
}
|
}
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
return super(AccountAdminMixin, self).add_view(request,
|
return super(AccountAdminMixin, self).add_view(request,
|
||||||
|
|
|
@ -2,18 +2,34 @@
|
||||||
{% load i18n admin_urls admin_static admin_modify %}
|
{% load i18n admin_urls admin_static admin_modify %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||||
|
{% if from_account %}
|
||||||
|
› <a href="{% url 'admin:app_list' app_label=account_opts.app_label %}">{{ account_opts.app_config.verbose_name }}</a>
|
||||||
|
› <a href="{% url account_opts|admin_urlname:'changelist' %}">{{ account_opts.verbose_name_plural|capfirst }}</a>
|
||||||
|
› <a href="{% url account_opts|admin_urlname:'change' account.pk|admin_urlquote %}">{{ account|truncatewords:"18" }}</a>
|
||||||
|
› {% if has_change_permission %}<a href="{% url opts|admin_urlname:'changelist' %}?account={{ account.pk }}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
|
||||||
|
{% else %}
|
||||||
|
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
|
||||||
|
› {% if has_change_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
› {% if add %}{% trans 'Add' %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block object-tools-items %}
|
{% block object-tools-items %}
|
||||||
|
{% if services %}
|
||||||
|
|
||||||
{% for service in services %}
|
{% for service in services %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url service|admin_urlname:'changelist' %}?account={{ original.pk }}" class="historylink">{{ service.verbose_name_plural|capfirst }}</a>
|
<a href="{% url service|admin_urlname:'changelist' %}?account={{ original.pk }}" class="historylink">{{ service.verbose_name_plural|capfirst }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">Account</h5>
|
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">Account</h5>
|
||||||
|
{% endif %}
|
||||||
|
{% if accounts %}
|
||||||
<ul class="object-tools">
|
<ul class="object-tools">
|
||||||
{% for account in accounts %}
|
{% for account in accounts %}
|
||||||
<li>
|
<li>
|
||||||
|
@ -21,10 +37,9 @@
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
|
||||||
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">a</h5>
|
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">a</h5>
|
||||||
|
|
||||||
<ul class="object-tools">
|
<ul class="object-tools">
|
||||||
|
{% endif %}
|
||||||
<li>
|
<li>
|
||||||
<a href="disable/" class="historylink">{% trans "Disable" %}</a>
|
<a href="disable/" class="historylink">{% trans "Disable" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -32,6 +47,5 @@
|
||||||
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
|
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
|
||||||
<a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
|
<a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
{% extends "admin/change_list.html" %}
|
||||||
|
{% load i18n admin_urls %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||||
|
{% if from_account %}
|
||||||
|
› <a href="{% url 'admin:app_list' app_label=account_opts.app_label %}">{{ account_opts.app_config.verbose_name }}</a>
|
||||||
|
› <a href="{% url account_opts|admin_urlname:'changelist' %}">{{ account_opts.verbose_name_plural|capfirst }}</a>
|
||||||
|
› <a href="{% url account_opts|admin_urlname:'change' account.pk|admin_urlquote %}">{{ account|truncatewords:"18" }}</a>
|
||||||
|
{% else %}
|
||||||
|
› <a href="{% url 'admin:app_list' app_label=cl.opts.app_label %}">{{ cl.opts.app_config.verbose_name }}</a>
|
||||||
|
{% endif %}
|
||||||
|
› {{ cl.opts.verbose_name_plural|capfirst }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block object-tools-items %}
|
||||||
|
{% if from_account %}
|
||||||
|
<li>
|
||||||
|
<a href="./" class="historylink">{% trans 'Show all' %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
{% url cl.opts|admin_urlname:'add' as add_url %}
|
||||||
|
<a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
|
||||||
|
{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endblock %}
|
|
@ -12,9 +12,16 @@ from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget,
|
||||||
|
|
||||||
class BillLineInline(admin.TabularInline):
|
class BillLineInline(admin.TabularInline):
|
||||||
model = BillLine
|
model = BillLine
|
||||||
|
fields = (
|
||||||
|
'description', 'initial_date', 'final_date', 'price', 'amount', 'tax'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BudgetLineInline(admin.TabularInline):
|
class BudgetLineInline(admin.TabularInline):
|
||||||
model = Budget
|
model = Budget
|
||||||
|
fields = (
|
||||||
|
'description', 'initial_date', 'final_date', 'price', 'amount', 'tax'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BillAdmin(AccountAdminMixin, admin.ModelAdmin):
|
class BillAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import AtLeastOneRequiredInlineFormSet
|
from orchestra.admin import AtLeastOneRequiredInlineFormSet
|
||||||
from orchestra.admin.utils import insertattr
|
from orchestra.admin.utils import insertattr
|
||||||
from orchestra.apps.accounts.admin import AccountAdmin, AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdmin, AccountAdminMixin
|
||||||
|
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
||||||
from .filters import HasInvoiceContactListFilter
|
from .filters import HasInvoiceContactListFilter
|
||||||
from .models import Contact, InvoiceContact
|
from .models import Contact, InvoiceContact
|
||||||
|
|
||||||
|
@ -19,6 +20,50 @@ class ContactAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
'contact__user__username', 'short_name', 'full_name', 'phone', 'phone2',
|
'contact__user__username', 'short_name', 'full_name', 'phone', 'phone2',
|
||||||
'email'
|
'email'
|
||||||
)
|
)
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('account_link', 'short_name', 'full_name')
|
||||||
|
}),
|
||||||
|
(_("Email"), {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('email', 'email_usage',)
|
||||||
|
}),
|
||||||
|
(_("Phone"), {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('phone', 'phone2'),
|
||||||
|
}),
|
||||||
|
(_("Postal address"), {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('address', ('zipcode', 'city'), 'country')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('account', 'short_name', 'full_name')
|
||||||
|
}),
|
||||||
|
(_("Email"), {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('email', 'email_usage',)
|
||||||
|
}),
|
||||||
|
(_("Phone"), {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('phone', 'phone_alternative'),
|
||||||
|
}),
|
||||||
|
(_("Postal address"), {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('address', ('zip_code', 'city'), 'country')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
|
""" Make value input widget bigger """
|
||||||
|
if db_field.name == 'address':
|
||||||
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
|
if db_field.name == 'email_usage':
|
||||||
|
kwargs['widget'] = paddingCheckboxSelectMultiple(130)
|
||||||
|
return super(ContactAdmin, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Contact, ContactAdmin)
|
admin.site.register(Contact, ContactAdmin)
|
||||||
|
@ -32,6 +77,8 @@ class InvoiceContactInline(admin.StackedInline):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
if db_field.name == 'address':
|
if db_field.name == 'address':
|
||||||
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
|
||||||
|
if db_field.name == 'email_usage':
|
||||||
|
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
|
||||||
return super(InvoiceContactInline, self).formfield_for_dbfield(db_field, **kwargs)
|
return super(InvoiceContactInline, self).formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +109,9 @@ insertattr(AccountAdmin, 'inlines', ContactInline)
|
||||||
insertattr(AccountAdmin, 'inlines', InvoiceContactInline)
|
insertattr(AccountAdmin, 'inlines', InvoiceContactInline)
|
||||||
insertattr(AccountAdmin, 'list_display', has_invoice)
|
insertattr(AccountAdmin, 'list_display', has_invoice)
|
||||||
insertattr(AccountAdmin, 'list_filter', HasInvoiceContactListFilter)
|
insertattr(AccountAdmin, 'list_filter', HasInvoiceContactListFilter)
|
||||||
for field in ('contacts__short_name', 'contacts__full_name', 'contacts__phone',
|
search_fields = (
|
||||||
'contacts__phone2', 'contacts__email'):
|
'contacts__short_name', 'contacts__full_name', 'contacts__phone',
|
||||||
|
'contacts__phone2', 'contacts__email'
|
||||||
|
)
|
||||||
|
for field in search_fields:
|
||||||
insertattr(AccountAdmin, 'search_fields', field)
|
insertattr(AccountAdmin, 'search_fields', field)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from orchestra.core import accounts
|
||||||
from orchestra.models.fields import MultiSelectField
|
from orchestra.models.fields import MultiSelectField
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
@ -15,8 +16,8 @@ class Contact(models.Model):
|
||||||
email_usage = MultiSelectField(_("email usage"), max_length=256, blank=True,
|
email_usage = MultiSelectField(_("email usage"), max_length=256, blank=True,
|
||||||
choices=settings.CONTACTS_EMAIL_USAGES,
|
choices=settings.CONTACTS_EMAIL_USAGES,
|
||||||
default=settings.CONTACTS_DEFAULT_EMAIL_USAGES)
|
default=settings.CONTACTS_DEFAULT_EMAIL_USAGES)
|
||||||
phone = models.CharField(_("Phone"), max_length=32, blank=True)
|
phone = models.CharField(_("phone"), max_length=32, blank=True)
|
||||||
phone2 = models.CharField(_("Alternative Phone"), max_length=32, blank=True)
|
phone2 = models.CharField(_("alternative phone"), max_length=32, blank=True)
|
||||||
address = models.TextField(_("address"), blank=True)
|
address = models.TextField(_("address"), blank=True)
|
||||||
city = models.CharField(_("city"), max_length=128, blank=True,
|
city = models.CharField(_("city"), max_length=128, blank=True,
|
||||||
default=settings.CONTACTS_DEFAULT_CITY)
|
default=settings.CONTACTS_DEFAULT_CITY)
|
||||||
|
@ -39,3 +40,6 @@ class InvoiceContact(models.Model):
|
||||||
country = models.CharField(_("country"), max_length=20,
|
country = models.CharField(_("country"), max_length=20,
|
||||||
default=settings.CONTACTS_DEFAULT_COUNTRY)
|
default=settings.CONTACTS_DEFAULT_COUNTRY)
|
||||||
vat = models.CharField(_("VAT number"), max_length=64)
|
vat = models.CharField(_("VAT number"), max_length=64)
|
||||||
|
|
||||||
|
|
||||||
|
accounts.register(Contact)
|
||||||
|
|
|
@ -88,7 +88,7 @@ class OrderAdmin(AccountAdminMixin, ChangeListDefaultFilter, admin.ModelAdmin):
|
||||||
('is_active', 'True'),
|
('is_active', 'True'),
|
||||||
)
|
)
|
||||||
|
|
||||||
content_object_link = admin_link('content_object')
|
content_object_link = admin_link('content_object', order=False)
|
||||||
display_registered_on = admin_date('registered_on')
|
display_registered_on = admin_date('registered_on')
|
||||||
display_cancelled_on = admin_date('cancelled_on')
|
display_cancelled_on = admin_date('cancelled_on')
|
||||||
|
|
||||||
|
|
|
@ -186,7 +186,7 @@ class Service(models.Model):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
content_type = self.handler.get_content_type()
|
content_type = self.handler.get_content_type()
|
||||||
if self.content_type != content_type:
|
if self.content_type != content_type:
|
||||||
msg =_("Content type must be equal to '%s'." % str(content_type))
|
msg =_("Content type must be equal to '%s'.") % str(content_type)
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
if not self.match:
|
if not self.match:
|
||||||
msg =_("Match should be provided")
|
msg =_("Match should be provided")
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from lxml import etree
|
||||||
|
from lxml.builder import E
|
||||||
|
|
||||||
|
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
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -44,6 +50,116 @@ class BankTransfer(PaymentMethod):
|
||||||
number_field = 'iban'
|
number_field = 'iban'
|
||||||
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):
|
||||||
|
for transaction in transactions:
|
||||||
|
account = transaction.account
|
||||||
|
data = transaction.data
|
||||||
|
transaction.info = self.payment_id
|
||||||
|
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.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")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
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)
|
||||||
|
self.set_id()
|
||||||
|
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(
|
||||||
|
E.CstmrDrctDbtInitn(
|
||||||
|
E.GrpHdr( # Group Header
|
||||||
|
E.MsgId(self.payment_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_at_02)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
E.PmtInf( # Payment Info
|
||||||
|
E.PmtInfId(self.payment_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.strfrm("%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
|
||||||
|
)
|
||||||
|
), {
|
||||||
|
'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')
|
||||||
|
schema.assertValid(sepa)
|
||||||
|
# TODO where to save this shit?
|
||||||
|
# TODO new model? Payment with batch support, How this relates to transaction?
|
||||||
|
return etree.tostring(page, pretty_print=True, xml_declaration=True)
|
||||||
|
|
||||||
|
|
||||||
class CreditCard(PaymentMethod):
|
class CreditCard(PaymentMethod):
|
||||||
|
|
|
@ -2,3 +2,13 @@ from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
PAYMENT_CURRENCY = getattr(settings, 'PAYMENT_CURRENCY', 'Eur')
|
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')
|
||||||
|
PAYMENTS_DD_CREDITOR_BIC = getattr(settings, 'PAYMENTS_DD_CREDITOR_BIC',
|
||||||
|
'InvalidBIC')
|
||||||
|
PAYMENTS_DD_CREDITOR_AT02_ID = getattr(settings, 'PAYMENTS_DD_CREDITOR_AT02_ID',
|
||||||
|
'InvalidAT02ID')
|
||||||
|
|
|
@ -89,7 +89,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
|
|
||||||
def display_mailboxes(self, address):
|
def display_mailboxes(self, address):
|
||||||
boxes = []
|
boxes = []
|
||||||
for mailbox in address.mailboxes():
|
for mailbox in address.mailboxes.all():
|
||||||
user = mailbox.user
|
user = mailbox.user
|
||||||
url = reverse('admin:users_user_mailbox_change', args=(user.pk,))
|
url = reverse('admin:users_user_mailbox_change', args=(user.pk,))
|
||||||
boxes.append('<a href="%s">%s</a>' % (url, user.username))
|
boxes.append('<a href="%s">%s</a>' % (url, user.username))
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
@ -20,9 +22,13 @@ class ShowTextWidget(forms.Widget):
|
||||||
else:
|
else:
|
||||||
final_value = '<br/>'.join(value.split('\n'))
|
final_value = '<br/>'.join(value.split('\n'))
|
||||||
if self.warning:
|
if self.warning:
|
||||||
final_value = u'<ul class="messagelist"><li class="warning">%s</li></ul>' %(final_value)
|
final_value = (
|
||||||
|
u'<ul class="messagelist"><li class="warning">%s</li></ul>'
|
||||||
|
% final_value)
|
||||||
if self.hidden:
|
if self.hidden:
|
||||||
final_value = u'%s<input type="hidden" name="%s" value="%s"/>' % (final_value, name, value)
|
final_value = (
|
||||||
|
u'%s<input type="hidden" name="%s" value="%s"/>'
|
||||||
|
% (final_value, name, value))
|
||||||
return mark_safe(final_value)
|
return mark_safe(final_value)
|
||||||
|
|
||||||
def _has_changed(self, initial, data):
|
def _has_changed(self, initial, data):
|
||||||
|
@ -44,3 +50,16 @@ class ReadOnlyWidget(forms.Widget):
|
||||||
|
|
||||||
def value_from_datadict(self, data, files, name):
|
def value_from_datadict(self, data, files, name):
|
||||||
return self.original_value
|
return self.original_value
|
||||||
|
|
||||||
|
|
||||||
|
def paddingCheckboxSelectMultiple(padding):
|
||||||
|
""" Ugly hack to render this widget nicely on Django admin """
|
||||||
|
widget = forms.CheckboxSelectMultiple()
|
||||||
|
old_render = widget.render
|
||||||
|
def render(self, *args, **kwargs):
|
||||||
|
value = old_render(self, *args, **kwargs)
|
||||||
|
value = re.sub(r'^<ul id=(.*)>',
|
||||||
|
r'<ul id=\1 style="padding-left:%ipx">' % padding, value, 1)
|
||||||
|
return mark_safe(value)
|
||||||
|
widget.render = render
|
||||||
|
return widget
|
||||||
|
|
Loading…
Reference in New Issue