Preliminar implementation of payment methods
This commit is contained in:
parent
72ef63ffdf
commit
bef7af084b
|
@ -62,6 +62,8 @@ def get_account_items():
|
||||||
if isinstalled('orchestra.apps.payments'):
|
if isinstalled('orchestra.apps.payments'):
|
||||||
url = reverse('admin:payments_transaction_changelist')
|
url = reverse('admin:payments_transaction_changelist')
|
||||||
childrens.append(items.MenuItem(_("Transactions"), url))
|
childrens.append(items.MenuItem(_("Transactions"), url))
|
||||||
|
url = reverse('admin:payments_paymentsource_changelist')
|
||||||
|
childrens.append(items.MenuItem(_("Payment Sources"), url))
|
||||||
if isinstalled('orchestra.apps.issues'):
|
if isinstalled('orchestra.apps.issues'):
|
||||||
url = reverse('admin:issues_ticket_changelist')
|
url = reverse('admin:issues_ticket_changelist')
|
||||||
childrens.append(items.MenuItem(_("Tickets"), url))
|
childrens.append(items.MenuItem(_("Tickets"), url))
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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
|
||||||
from orchestra.core import services
|
from orchestra.core import services, accounts
|
||||||
|
|
||||||
from .filters import HasMainUserListFilter
|
from .filters import HasMainUserListFilter
|
||||||
from .forms import AccountCreationForm, AccountChangeForm
|
from .forms import AccountCreationForm, AccountChangeForm
|
||||||
|
@ -60,10 +60,13 @@ class AccountAdmin(ExtendedModelAdmin):
|
||||||
if not account.is_active:
|
if not account.is_active:
|
||||||
messages.warning(request, 'This account is disabled.')
|
messages.warning(request, 'This account is disabled.')
|
||||||
context = {
|
context = {
|
||||||
# TODO not services but everythin (payments, bills, etc)
|
|
||||||
'services': sorted(
|
'services': sorted(
|
||||||
[ model._meta for model in services.get() if model is not Account ],
|
[ model._meta for model in services.get() if model is not Account ],
|
||||||
key=lambda i: i.verbose_name_plural.lower()
|
key=lambda i: i.verbose_name_plural.lower()
|
||||||
|
),
|
||||||
|
'accounts': sorted(
|
||||||
|
[ model._meta for model in accounts.get() if model is not Account ],
|
||||||
|
key=lambda i: i.verbose_name_plural.lower()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
|
@ -83,8 +86,9 @@ class AccountAdmin(ExtendedModelAdmin):
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
""" Select related for performance """
|
""" Select related for performance """
|
||||||
# TODO move invoicecontact to contacts
|
# TODO move invoicecontact to contacts
|
||||||
|
qs = super(AccountAdmin, self).get_queryset(request)
|
||||||
related = ('user', 'invoicecontact')
|
related = ('user', 'invoicecontact')
|
||||||
return super(AccountAdmin, self).get_queryset(request).select_related(*related)
|
return qs.select_related(*related)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Account, AccountAdmin)
|
admin.site.register(Account, AccountAdmin)
|
||||||
|
|
|
@ -3,11 +3,28 @@
|
||||||
|
|
||||||
|
|
||||||
{% block object-tools-items %}
|
{% block object-tools-items %}
|
||||||
|
|
||||||
|
|
||||||
{% 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>
|
||||||
|
|
||||||
|
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">Account</h5>
|
||||||
|
<ul class="object-tools">
|
||||||
|
{% for account in accounts %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url account|admin_urlname:'changelist' %}?account={{ original.pk }}" class="historylink">{{ account.verbose_name_plural|capfirst }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<h5 style="visibility:hidden; margin: 1.5em 1.5em 0;">a</h5>
|
||||||
|
|
||||||
|
<ul class="object-tools">
|
||||||
<li>
|
<li>
|
||||||
<a href="disable/" class="historylink">{% trans "Disable" %}</a>
|
<a href="disable/" class="historylink">{% trans "Disable" %}</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -15,6 +32,6 @@
|
||||||
{% 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>
|
||||||
{% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ from django.db import models
|
||||||
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 orchestra.core import accounts
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,6 +89,10 @@ class Bill(models.Model):
|
||||||
self.ident = '{prefix}{year}{number}'.format(
|
self.ident = '{prefix}{year}{number}'.format(
|
||||||
prefix=prefix, year=year, number=number)
|
prefix=prefix, year=year, number=number)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.status = self.CLOSED
|
||||||
|
self.save()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.bill_type:
|
if not self.bill_type:
|
||||||
self.bill_type = type(self).get_type()
|
self.bill_type = type(self).get_type()
|
||||||
|
@ -146,3 +152,5 @@ class BillLine(BaseBillLine):
|
||||||
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
|
||||||
related_name='amendment_lines', null=True, blank=True)
|
related_name='amendment_lines', null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
accounts.register(Bill)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
||||||
|
|
||||||
from .models import Bill, BillLine
|
from .models import Bill, BillLine
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,8 +10,13 @@ class BillLineSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
model = BillLine
|
model = BillLine
|
||||||
|
|
||||||
|
|
||||||
class BillSerializer(serializers.HyperlinkedModelSerializer):
|
|
||||||
|
class BillSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||||
lines = BillLineSerializer(source='billlines')
|
lines = BillLineSerializer(source='billlines')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bill
|
model = Bill
|
||||||
|
fields = (
|
||||||
|
'url', 'ident', 'bill_type', 'status', 'created_on', 'due_on',
|
||||||
|
'comments', 'html', 'lines'
|
||||||
|
)
|
||||||
|
|
|
@ -30,10 +30,10 @@ class MailmanTraffic(ServiceMonitor):
|
||||||
MAILMAN_LOG="$4"
|
MAILMAN_LOG="$4"
|
||||||
|
|
||||||
SUBSCRIBERS=$(list_members ${LIST_NAME} | wc -l)
|
SUBSCRIBERS=$(list_members ${LIST_NAME} | wc -l)
|
||||||
SIZE=$(grep ' post to ${LIST_NAME} ' "${MAILMAN_LOG}" \
|
SIZE=$(grep ' post to ${LIST_NAME} ' "${MAILMAN_LOG}" \\
|
||||||
| awk '\"$LAST_DATE\"<=$0 && $0<=\"%s\"' \
|
| awk '"$LAST_DATE"<=$0 && $0<="%s"' \\
|
||||||
| sed 's/.*size=\([0-9]*\).*/\\1/' \
|
| sed 's/.*size=\([0-9]*\).*/\\1/' \\
|
||||||
| tr '\\n' '+' \
|
| tr '\\n' '+' \\
|
||||||
| xargs -i echo {} )
|
| xargs -i echo {} )
|
||||||
echo ${OBJECT_ID} $(( ${SIZE}*${SUBSCRIBERS} ))
|
echo ${OBJECT_ID} $(( ${SIZE}*${SUBSCRIBERS} ))
|
||||||
}""" % current_date))
|
}""" % current_date))
|
||||||
|
|
|
@ -5,7 +5,7 @@ from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
||||||
from .models import List
|
from .models import List
|
||||||
|
|
||||||
|
|
||||||
class ListSerializer(AccountSerializerMixin, serializers.ModelSerializer):
|
class ListSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = List
|
model = List
|
||||||
fields = ('name', 'address_name', 'address_domain',)
|
fields = ('url', 'name', 'address_name', 'address_domain')
|
||||||
|
|
|
@ -71,6 +71,13 @@ class BackendOperationInline(admin.TabularInline):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def display_mono(field):
|
||||||
|
def display(self, log):
|
||||||
|
return monospace_format(escape(getattr(log, field)))
|
||||||
|
display.short_description = _(field)
|
||||||
|
return display
|
||||||
|
|
||||||
|
|
||||||
class BackendLogAdmin(admin.ModelAdmin):
|
class BackendLogAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'id', 'backend', 'server_link', 'display_state', 'exit_code',
|
'id', 'backend', 'server_link', 'display_state', 'exit_code',
|
||||||
|
@ -91,22 +98,10 @@ class BackendLogAdmin(admin.ModelAdmin):
|
||||||
display_last_update = admin_date('last_update')
|
display_last_update = admin_date('last_update')
|
||||||
display_created = admin_date('created')
|
display_created = admin_date('created')
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||||
|
mono_script = display_mono('script')
|
||||||
def mono_script(self, log):
|
mono_stdout = display_mono('stdout')
|
||||||
return monospace_format(escape(log.script))
|
mono_stderr = display_mono('stderr')
|
||||||
mono_script.short_description = _("script")
|
mono_traceback = display_mono('traceback')
|
||||||
|
|
||||||
def mono_stdout(self, log):
|
|
||||||
return monospace_format(escape(log.stdout))
|
|
||||||
mono_stdout.short_description = _("stdout")
|
|
||||||
|
|
||||||
def mono_stderr(self, log):
|
|
||||||
return monospace_format(escape(log.stderr))
|
|
||||||
mono_stderr.short_description = _("stderr")
|
|
||||||
|
|
||||||
def mono_traceback(self, log):
|
|
||||||
return monospace_format(escape(log.traceback))
|
|
||||||
mono_traceback.short_description = _("traceback")
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
""" Order by structured name and imporve performance """
|
""" Order by structured name and imporve performance """
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
from rest_framework import viewsets
|
||||||
|
|
||||||
|
from orchestra.api import router
|
||||||
|
from orchestra.apps.accounts.api import AccountApiMixin
|
||||||
|
|
||||||
|
from .models import Order
|
||||||
|
from .serializers import OrderSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class OrderViewSet(AccountApiMixin, viewsets.ModelViewSet):
|
||||||
|
model = Order
|
||||||
|
serializer_class = OrderSerializer
|
||||||
|
|
||||||
|
router.register(r'orders', OrderViewSet)
|
|
@ -10,7 +10,7 @@ from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
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 caches, services
|
from orchestra.core import caches, services, accounts
|
||||||
from orchestra.models import queryset
|
from orchestra.models import queryset
|
||||||
from orchestra.utils.apps import autodiscover
|
from orchestra.utils.apps import autodiscover
|
||||||
|
|
||||||
|
@ -175,7 +175,8 @@ class Service(models.Model):
|
||||||
return services
|
return services
|
||||||
|
|
||||||
# FIXME some times caching is nasty, do we really have to? make get_plugin more efficient?
|
# FIXME some times caching is nasty, do we really have to? make get_plugin more efficient?
|
||||||
# @cached_property
|
# @property
|
||||||
|
@cached_property
|
||||||
def handler(self):
|
def handler(self):
|
||||||
""" Accessor of this service handler instance """
|
""" Accessor of this service handler instance """
|
||||||
if self.handler_type:
|
if self.handler_type:
|
||||||
|
@ -338,3 +339,6 @@ def update_orders(sender, **kwargs):
|
||||||
related = helpers.get_related_objects(instance)
|
related = helpers.get_related_objects(instance)
|
||||||
if related:
|
if related:
|
||||||
Order.update_orders(related)
|
Order.update_orders(related)
|
||||||
|
|
||||||
|
|
||||||
|
accounts.register(Order)
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
||||||
|
|
||||||
|
from .models import Order
|
||||||
|
|
||||||
|
|
||||||
|
class OrderSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Order
|
||||||
|
fields = (
|
||||||
|
'url', 'registered_on', 'cancelled_on', 'billed_on', 'billed_until',
|
||||||
|
'description'
|
||||||
|
)
|
|
@ -1,7 +1,9 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
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 .methods import BankTransfer
|
||||||
from .models import PaymentSource, Transaction
|
from .models import PaymentSource, Transaction
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,14 +19,21 @@ STATE_COLORS = {
|
||||||
|
|
||||||
class TransactionAdmin(admin.ModelAdmin):
|
class TransactionAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'id', 'bill_link', 'account_link', 'method', 'display_state', 'amount'
|
'id', 'bill_link', 'account_link', 'source', 'display_state', 'amount'
|
||||||
)
|
)
|
||||||
list_filter = ('method', 'state')
|
list_filter = ('source__method', 'state')
|
||||||
|
|
||||||
bill_link = admin_link('bill')
|
bill_link = admin_link('bill')
|
||||||
account_link = admin_link('bill__account')
|
account_link = admin_link('bill__account')
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(PaymentSource)
|
class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
|
list_display = ('label', 'method', 'number', 'account_link', 'is_active')
|
||||||
|
list_filter = ('method', 'is_active')
|
||||||
|
form = BankTransfer().get_form()
|
||||||
|
# TODO select payment source method
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(PaymentSource, PaymentSourceAdmin)
|
||||||
admin.site.register(Transaction, TransactionAdmin)
|
admin.site.register(Transaction, TransactionAdmin)
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django_iban.forms import IBANFormField
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentSourceDataForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
exclude = ('data', 'method')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(PaymentSourceDataForm, self).__init__(*args, **kwargs)
|
||||||
|
instance = kwargs.get('instance')
|
||||||
|
if instance:
|
||||||
|
for field in self.declared_fields:
|
||||||
|
initial = self.fields[field].initial
|
||||||
|
self.fields[field].initial = instance.data.get(field, initial)
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
plugin = self.plugin
|
||||||
|
self.instance.method = plugin.get_plugin_name()
|
||||||
|
self.instance.data = {
|
||||||
|
field: self.cleaned_data[field] for field in self.declared_fields
|
||||||
|
}
|
||||||
|
return super(PaymentSourceDataForm, self).save(commit=commit)
|
||||||
|
|
||||||
|
|
||||||
|
class BankTransferForm(PaymentSourceDataForm):
|
||||||
|
iban = IBANFormField(label='IBAN',
|
||||||
|
widget=forms.TextInput(attrs={'size': '50'}))
|
||||||
|
name = forms.CharField(max_length=128, label=_("Name"),
|
||||||
|
widget=forms.TextInput(attrs={'size': '50'}))
|
||||||
|
|
||||||
|
|
||||||
|
class CreditCardForm(PaymentSourceDataForm):
|
||||||
|
label = forms.CharField(max_length=128, label=_("Label"),
|
||||||
|
help_text=_("Use a name such as \"Jo's Visa\" to remember which "
|
||||||
|
"card it is."))
|
||||||
|
first_name = forms.CharField(max_length=128)
|
||||||
|
last_name = forms.CharField(max_length=128)
|
||||||
|
address = forms.CharField(max_length=128)
|
||||||
|
zip = forms.CharField(max_length=128)
|
||||||
|
city = forms.CharField(max_length=128)
|
||||||
|
country = forms.CharField(max_length=128)
|
||||||
|
card_number = forms.CharField(max_length=128)
|
||||||
|
expiration_date = forms.CharField(max_length=128)
|
||||||
|
security_code = forms.CharField(max_length=128)
|
|
@ -1,15 +1,52 @@
|
||||||
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 rest_framework import serializers
|
||||||
|
|
||||||
from orchestra.utils import plugins
|
from orchestra.utils import plugins
|
||||||
|
|
||||||
|
from .forms import BankTransferForm, CreditCardForm
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethod(plugins.Plugin):
|
class PaymentMethod(plugins.Plugin):
|
||||||
|
label_field = 'label'
|
||||||
|
number_field = 'number'
|
||||||
|
|
||||||
__metaclass__ = plugins.PluginMount
|
__metaclass__ = plugins.PluginMount
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
self.form.plugin = self
|
||||||
|
return self.form
|
||||||
|
|
||||||
|
def get_serializer(self):
|
||||||
|
self.serializer.plugin = self
|
||||||
|
return self.serializer
|
||||||
|
|
||||||
|
def get_label(self, data):
|
||||||
|
return data[self.label_field]
|
||||||
|
|
||||||
|
def get_number(self, data):
|
||||||
|
return data[self.number_field]
|
||||||
|
|
||||||
|
|
||||||
|
class BankTransferSerializer(serializers.Serializer):
|
||||||
|
iban = serializers.CharField(label='IBAN', validators=[IBANValidator()],
|
||||||
|
min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34)
|
||||||
|
name = serializers.CharField(label=_("Name"), max_length=128)
|
||||||
|
|
||||||
|
|
||||||
|
class CreditCardSerializer(serializers.Serializer):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BankTransfer(PaymentMethod):
|
class BankTransfer(PaymentMethod):
|
||||||
verbose_name = _("Bank transfer")
|
verbose_name = _("Bank transfer")
|
||||||
|
label_field = 'name'
|
||||||
|
number_field = 'iban'
|
||||||
|
form = BankTransferForm
|
||||||
|
serializer = BankTransferSerializer
|
||||||
|
|
||||||
|
|
||||||
class CreditCard(PaymentMethod):
|
class CreditCard(PaymentMethod):
|
||||||
verbose_name = _("Credit card")
|
verbose_name = _("Credit card")
|
||||||
|
form = CreditCardForm
|
||||||
|
serializer = CreditCardSerializer
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
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 jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
|
|
||||||
|
from orchestra.core import accounts
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
from .methods import PaymentMethod
|
from .methods import PaymentMethod
|
||||||
|
|
||||||
|
@ -14,6 +18,25 @@ class PaymentSource(models.Model):
|
||||||
data = JSONField(_("data"))
|
data = JSONField(_("data"))
|
||||||
is_active = models.BooleanField(_("is active"), default=True)
|
is_active = models.BooleanField(_("is active"), default=True)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.label or str(self.account)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def label(self):
|
||||||
|
try:
|
||||||
|
plugin = PaymentMethod.get_plugin(self.method)()
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
return plugin.get_label(self.data)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def number(self):
|
||||||
|
try:
|
||||||
|
plugin = PaymentMethod.get_plugin(self.method)()
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
return plugin.get_number(self.data)
|
||||||
|
|
||||||
|
|
||||||
class Transaction(models.Model):
|
class Transaction(models.Model):
|
||||||
WAITTING_PROCESSING = 'WAITTING_PROCESSING'
|
WAITTING_PROCESSING = 'WAITTING_PROCESSING'
|
||||||
|
@ -34,8 +57,8 @@ class Transaction(models.Model):
|
||||||
# TODO account fk?
|
# TODO account fk?
|
||||||
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"),
|
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"),
|
||||||
related_name='transactions')
|
related_name='transactions')
|
||||||
method = models.CharField(_("payment method"), max_length=32,
|
source = models.ForeignKey(PaymentSource, verbose_name=_("source"),
|
||||||
choices=PaymentMethod.get_plugin_choices())
|
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"))
|
data = JSONField(_("data"))
|
||||||
|
@ -47,3 +70,7 @@ class Transaction(models.Model):
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return "Transaction {}".format(self.id)
|
return "Transaction {}".format(self.id)
|
||||||
|
|
||||||
|
|
||||||
|
accounts.register(PaymentSource)
|
||||||
|
accounts.register(Transaction)
|
||||||
|
|
|
@ -1,14 +1,42 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import PaymentSource, PaymentSource
|
from orchestra.apps.accounts.serializers import AccountSerializerMixin
|
||||||
|
|
||||||
|
from .methods import PaymentMethod
|
||||||
|
from .models import PaymentSource, Transaction
|
||||||
|
|
||||||
|
|
||||||
class PaymentSourceSerializer(serializers.HyperlinkedModelSerializer):
|
class PaymentSourceSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PaymentSource
|
model = PaymentSource
|
||||||
|
fields = ('url', 'method', 'data', 'is_active')
|
||||||
|
|
||||||
|
def validate_data(self, attrs, source):
|
||||||
|
plugin = PaymentMethod.get_plugin(attrs['method'])
|
||||||
|
serializer_class = plugin().get_serializer()
|
||||||
|
serializer = serializer_class(data=attrs[source])
|
||||||
|
if not serializer.is_valid():
|
||||||
|
raise serializers.ValidationError(serializer.errors)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def transform_data(self, obj, value):
|
||||||
|
if not obj:
|
||||||
|
return {}
|
||||||
|
if obj.method:
|
||||||
|
plugin = PaymentMethod.get_plugin(obj.method)
|
||||||
|
serializer_class = plugin().get_serializer()
|
||||||
|
return serializer_class().to_native(obj.data)
|
||||||
|
return obj.data
|
||||||
|
|
||||||
|
def metadata(self):
|
||||||
|
meta = super(PaymentSourceSerializer, self).metadata()
|
||||||
|
meta['data'] = {
|
||||||
|
method.get_plugin_name(): method().get_serializer()().metadata()
|
||||||
|
for method in PaymentMethod.get_plugins()
|
||||||
|
}
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
class TransactionSerializer(serializers.HyperlinkedModelSerializer):
|
class TransactionSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PaymentSource
|
model = Transaction
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.db import models
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
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 accounts
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
|
|
||||||
|
@ -32,4 +32,4 @@ class Rate(models.Model):
|
||||||
return "{}-{}".format(str(self.value), self.quantity)
|
return "{}-{}".format(str(self.value), self.quantity)
|
||||||
|
|
||||||
|
|
||||||
services.register(Pack, menu=False)
|
accounts.register(Pack)
|
||||||
|
|
|
@ -25,10 +25,11 @@ class ResourceSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
if not running_syncdb():
|
if not running_syncdb():
|
||||||
# TODO why this is even loaded during syncdb?
|
# TODO why this is even loaded during syncdb?
|
||||||
|
# Create nested serializers on target models
|
||||||
for ct, resources in Resource.objects.group_by('content_type'):
|
for ct, resources in Resource.objects.group_by('content_type'):
|
||||||
model = ct.model_class()
|
model = ct.model_class()
|
||||||
try:
|
try:
|
||||||
router.insert(model, 'resources', ResourceSerializer, required=False, many=True)
|
router.insert(model, 'resources', ResourceSerializer, required=False, many=True, source='resource_set')
|
||||||
except KeyError:
|
except KeyError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ class SystemUserBackend(ServiceController):
|
||||||
if [[ $( id %(username)s ) ]]; then
|
if [[ $( id %(username)s ) ]]; then
|
||||||
usermod --password '%(password)s' %(username)s
|
usermod --password '%(password)s' %(username)s
|
||||||
else
|
else
|
||||||
useradd %(username)s --password '%(password)s' \
|
useradd %(username)s --password '%(password)s' \\
|
||||||
--shell /bin/false
|
--shell /bin/false
|
||||||
fi
|
fi
|
||||||
mkdir -p %(home)s
|
mkdir -p %(home)s
|
||||||
|
@ -74,8 +74,8 @@ class FTPTraffic(ServiceMonitor):
|
||||||
INI_DATE=$2
|
INI_DATE=$2
|
||||||
USERNAME="$3"
|
USERNAME="$3"
|
||||||
LOG_FILE="$4"
|
LOG_FILE="$4"
|
||||||
grep "UPLOAD\|DOWNLOAD" "${LOG_FILE}" \
|
grep "UPLOAD\|DOWNLOAD" "${LOG_FILE}" \\
|
||||||
| grep " \\[${USERNAME}\\] " \
|
| grep " \\[${USERNAME}\\] " \\
|
||||||
| awk -v ini="${INI_DATE}" '
|
| awk -v ini="${INI_DATE}" '
|
||||||
BEGIN {
|
BEGIN {
|
||||||
end = "%s"
|
end = "%s"
|
||||||
|
|
|
@ -24,20 +24,20 @@ class Apache2Backend(ServiceController):
|
||||||
extra_conf += self.get_security(site)
|
extra_conf += self.get_security(site)
|
||||||
context['extra_conf'] = extra_conf
|
context['extra_conf'] = extra_conf
|
||||||
|
|
||||||
apache_conf = Template(
|
apache_conf = Template(textwrap.dedent("""\
|
||||||
"# {{ banner }}\n"
|
# {{ banner }}
|
||||||
"<VirtualHost *:{{ site.port }}>\n"
|
<VirtualHost *:{{ site.port }}>
|
||||||
" ServerName {{ site.domains.all|first }}\n"
|
ServerName {{ site.domains.all|first }}
|
||||||
"{% if site.domains.all|slice:\"1:\" %}"
|
{% if site.domains.all|slice:"1:" %}
|
||||||
" ServerAlias {{ site.domains.all|slice:\"1:\"|join:' ' }}\n"
|
ServerAlias {{ site.domains.all|slice:"1:"|join:' ' }}
|
||||||
"{% endif %}"
|
{% endif %}
|
||||||
" CustomLog {{ logs }} common\n"
|
CustomLog {{ logs }} common
|
||||||
" SuexecUserGroup {{ user }} {{ group }}\n"
|
SuexecUserGroup {{ user }} {{ group }}
|
||||||
"{% for line in extra_conf.splitlines %}"
|
{% for line in extra_conf.splitlines %}"
|
||||||
" {{ line | safe }}\n"
|
{{ line | safe }}
|
||||||
"{% endfor %}"
|
{% endfor %}
|
||||||
"</VirtualHost>\n"
|
</VirtualHost>"""
|
||||||
)
|
))
|
||||||
apache_conf = apache_conf.render(Context(context))
|
apache_conf = apache_conf.render(Context(context))
|
||||||
apache_conf += self.get_protections(site)
|
apache_conf += self.get_protections(site)
|
||||||
context['apache_conf'] = apache_conf
|
context['apache_conf'] = apache_conf
|
||||||
|
|
|
@ -144,6 +144,7 @@ FLUENT_DASHBOARD_APP_GROUPS = (
|
||||||
'orchestra.apps.orders.models.Order',
|
'orchestra.apps.orders.models.Order',
|
||||||
'orchestra.apps.prices.models.Pack',
|
'orchestra.apps.prices.models.Pack',
|
||||||
'orchestra.apps.bills.models.Bill',
|
'orchestra.apps.bills.models.Bill',
|
||||||
|
# 'orchestra.apps.payments.models.PaymentSource',
|
||||||
'orchestra.apps.payments.models.Transaction',
|
'orchestra.apps.payments.models.Transaction',
|
||||||
'orchestra.apps.issues.models.Ticket',
|
'orchestra.apps.issues.models.Ticket',
|
||||||
),
|
),
|
||||||
|
@ -184,6 +185,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
|
||||||
'orders/service': 'price.png',
|
'orders/service': 'price.png',
|
||||||
'prices/pack': 'Pack.png',
|
'prices/pack': 'Pack.png',
|
||||||
'bills/bill': 'invoice.png',
|
'bills/bill': 'invoice.png',
|
||||||
|
'payments/paymentsource': 'card_in_use.png',
|
||||||
'payments/transaction': 'transaction.png',
|
'payments/transaction': 'transaction.png',
|
||||||
'issues/ticket': 'Ticket_star.png',
|
'issues/ticket': 'Ticket_star.png',
|
||||||
# Administration
|
# Administration
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
class Service(object):
|
class Register(object):
|
||||||
_registry = {}
|
def __init__(self):
|
||||||
|
self._registry = {}
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
return key in self._registry
|
return key in self._registry
|
||||||
|
@ -18,4 +19,6 @@ class Service(object):
|
||||||
return self._registry
|
return self._registry
|
||||||
|
|
||||||
|
|
||||||
services = Service()
|
services = Register()
|
||||||
|
# TODO rename to something else
|
||||||
|
accounts = Register()
|
||||||
|
|
|
@ -1,38 +1,21 @@
|
||||||
def group_by(qset, *fields):
|
def group_by(qset, *fields, **kwargs):
|
||||||
""" group_by iterator with support for multiple nested fields """
|
""" group_by iterator with support for multiple nested fields """
|
||||||
def nest(objects, ix):
|
ix = kwargs.get('ix', 0)
|
||||||
objs = []
|
if ix is 0:
|
||||||
result = []
|
qset = qset.order_by(*fields)
|
||||||
first = True
|
group = []
|
||||||
for obj in objects:
|
|
||||||
current = getattr(obj, fields[ix])
|
|
||||||
if first or current == previous:
|
|
||||||
objs.append(obj)
|
|
||||||
else:
|
|
||||||
if ix < len(fields)-1:
|
|
||||||
objs = nest(list(objs), ix+1)
|
|
||||||
result.append((previous, objs))
|
|
||||||
objs = [obj]
|
|
||||||
previous = current
|
|
||||||
first = False
|
|
||||||
if ix < len(fields)-1:
|
|
||||||
objs = nest(list(objs), ix+1)
|
|
||||||
result.append((current, objs))
|
|
||||||
return result
|
|
||||||
|
|
||||||
objs = []
|
|
||||||
first = True
|
first = True
|
||||||
for obj in qset.order_by(*fields):
|
for obj in qset:
|
||||||
current = getattr(obj, fields[0])
|
current = getattr(obj, fields[ix])
|
||||||
if first or current == previous:
|
if first or current == previous:
|
||||||
objs.append(obj)
|
group.append(obj)
|
||||||
else:
|
else:
|
||||||
if len(fields) > 1:
|
if ix < len(fields)-1:
|
||||||
objs = nest(objs, 1)
|
group = group_by(group, *fields, ix=ix+1)
|
||||||
yield previous, objs
|
yield previous, group
|
||||||
objs = [obj]
|
group = [obj]
|
||||||
previous = current
|
previous = current
|
||||||
first = False
|
first = False
|
||||||
if len(fields) > 1:
|
if ix < len(fields)-1:
|
||||||
objs = nest(objs, 1)
|
group = group_by(group, *fields, ix=ix+1)
|
||||||
yield current, objs
|
yield previous, group
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,53 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
enable-background="new 0 0 24 24"
|
||||||
|
id="Layer_1"
|
||||||
|
version="1.0"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="0.48.3.1 r9886"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
sodipodi:docname="card_in_use.svg"
|
||||||
|
inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/icons/card_in_use.png"
|
||||||
|
inkscape:export-xdpi="90"
|
||||||
|
inkscape:export-ydpi="90"><metadata
|
||||||
|
id="metadata11"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs9" /><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1024"
|
||||||
|
id="namedview7"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="6.9532167"
|
||||||
|
inkscape:cx="8.2115642"
|
||||||
|
inkscape:cy="-0.18909649"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="27"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="Layer_1" /><path
|
||||||
|
d="m 40.305084,9.1949147 h -20 c -2.2,0 -4,1.8000003 -4,4.0000003 v 14 c 0,0 10.6,-6.6 13.2,-4 2.6,2.6 -2.6,8 -2.6,8 h 13.4 c 2.2,0 4,-1.8 4,-4 v -14 c 0,-2.2 -1.8,-4.0000003 -4,-4.0000003 z m 0,8.0000003 h -20 v -4 h 20 v 4 z"
|
||||||
|
id="path3"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#2e3436" /><path
|
||||||
|
d="m 17.905084,13.194915 c -1.2,0 -3.2,0.6 -5.4,2.6 l -6.1999996,5.4 v 16 h 7.5999996 c 6.8,0 8.4,-2 11.4,-4.8 3,-2.8 7.2,-5.8 4.2,-9.2 -1.8,-2.2 -4.2,-1.8 -8.4,1.2 -2.2,1.4 -4.2,2.8 -4.2,2.8"
|
||||||
|
stroke-miterlimit="10"
|
||||||
|
id="path5"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:none;stroke:#2e3436;stroke-width:4.00000047999999975;stroke-linecap:round;stroke-miterlimit:10" /></svg>
|
After Width: | Height: | Size: 2.3 KiB |
Loading…
Reference in New Issue