Preliminar implementation of payment methods

This commit is contained in:
Marc 2014-07-28 17:28:00 +00:00
parent 72ef63ffdf
commit bef7af084b
25 changed files with 349 additions and 95 deletions

View File

@ -62,6 +62,8 @@ def get_account_items():
if isinstalled('orchestra.apps.payments'):
url = reverse('admin:payments_transaction_changelist')
childrens.append(items.MenuItem(_("Transactions"), url))
url = reverse('admin:payments_paymentsource_changelist')
childrens.append(items.MenuItem(_("Payment Sources"), url))
if isinstalled('orchestra.apps.issues'):
url = reverse('admin:issues_ticket_changelist')
childrens.append(items.MenuItem(_("Tickets"), url))

View File

@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
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 .forms import AccountCreationForm, AccountChangeForm
@ -60,10 +60,13 @@ class AccountAdmin(ExtendedModelAdmin):
if not account.is_active:
messages.warning(request, 'This account is disabled.')
context = {
# TODO not services but everythin (payments, bills, etc)
'services': sorted(
[ model._meta for model in services.get() if model is not Account ],
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 {})
@ -83,8 +86,9 @@ class AccountAdmin(ExtendedModelAdmin):
def get_queryset(self, request):
""" Select related for performance """
# TODO move invoicecontact to contacts
qs = super(AccountAdmin, self).get_queryset(request)
related = ('user', 'invoicecontact')
return super(AccountAdmin, self).get_queryset(request).select_related(*related)
return qs.select_related(*related)
admin.site.register(Account, AccountAdmin)

View File

@ -3,11 +3,28 @@
{% block object-tools-items %}
{% for service in services %}
<li>
<a href="{% url service|admin_urlname:'changelist' %}?account={{ original.pk }}" class="historylink">{{ service.verbose_name_plural|capfirst }}</a>
</li>
{% 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>
<a href="disable/" class="historylink">{% trans "Disable" %}</a>
</li>
@ -15,6 +32,6 @@
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
<a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
</li>
{% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
{% endblock %}

View File

@ -2,6 +2,8 @@ from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.core import accounts
from . import settings
@ -87,6 +89,10 @@ class Bill(models.Model):
self.ident = '{prefix}{year}{number}'.format(
prefix=prefix, year=year, number=number)
def close(self):
self.status = self.CLOSED
self.save()
def save(self, *args, **kwargs):
if not self.bill_type:
self.bill_type = type(self).get_type()
@ -146,3 +152,5 @@ class BillLine(BaseBillLine):
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True)
accounts.register(Bill)

View File

@ -1,5 +1,7 @@
from rest_framework import serializers
from orchestra.apps.accounts.serializers import AccountSerializerMixin
from .models import Bill, BillLine
@ -8,8 +10,13 @@ class BillLineSerializer(serializers.HyperlinkedModelSerializer):
model = BillLine
class BillSerializer(serializers.HyperlinkedModelSerializer):
class BillSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
lines = BillLineSerializer(source='billlines')
class Meta:
model = Bill
fields = (
'url', 'ident', 'bill_type', 'status', 'created_on', 'due_on',
'comments', 'html', 'lines'
)

View File

@ -30,10 +30,10 @@ class MailmanTraffic(ServiceMonitor):
MAILMAN_LOG="$4"
SUBSCRIBERS=$(list_members ${LIST_NAME} | wc -l)
SIZE=$(grep ' post to ${LIST_NAME} ' "${MAILMAN_LOG}" \
| awk '\"$LAST_DATE\"<=$0 && $0<=\"%s\"' \
| sed 's/.*size=\([0-9]*\).*/\\1/' \
| tr '\\n' '+' \
SIZE=$(grep ' post to ${LIST_NAME} ' "${MAILMAN_LOG}" \\
| awk '"$LAST_DATE"<=$0 && $0<="%s"' \\
| sed 's/.*size=\([0-9]*\).*/\\1/' \\
| tr '\\n' '+' \\
| xargs -i echo {} )
echo ${OBJECT_ID} $(( ${SIZE}*${SUBSCRIBERS} ))
}""" % current_date))

View File

@ -5,7 +5,7 @@ from orchestra.apps.accounts.serializers import AccountSerializerMixin
from .models import List
class ListSerializer(AccountSerializerMixin, serializers.ModelSerializer):
class ListSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
class Meta:
model = List
fields = ('name', 'address_name', 'address_domain',)
fields = ('url', 'name', 'address_name', 'address_domain')

View File

@ -71,6 +71,13 @@ class BackendOperationInline(admin.TabularInline):
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):
list_display = (
'id', 'backend', 'server_link', 'display_state', 'exit_code',
@ -91,22 +98,10 @@ class BackendLogAdmin(admin.ModelAdmin):
display_last_update = admin_date('last_update')
display_created = admin_date('created')
display_state = admin_colored('state', colors=STATE_COLORS)
def mono_script(self, log):
return monospace_format(escape(log.script))
mono_script.short_description = _("script")
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")
mono_script = display_mono('script')
mono_stdout = display_mono('stdout')
mono_stderr = display_mono('stderr')
mono_traceback = display_mono('traceback')
def get_queryset(self, request):
""" Order by structured name and imporve performance """

View File

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

View File

@ -10,7 +10,7 @@ from django.utils import timezone
from django.utils.functional import cached_property
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.utils.apps import autodiscover
@ -175,7 +175,8 @@ class Service(models.Model):
return services
# FIXME some times caching is nasty, do we really have to? make get_plugin more efficient?
# @cached_property
# @property
@cached_property
def handler(self):
""" Accessor of this service handler instance """
if self.handler_type:
@ -338,3 +339,6 @@ def update_orders(sender, **kwargs):
related = helpers.get_related_objects(instance)
if related:
Order.update_orders(related)
accounts.register(Order)

View File

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

View File

@ -1,7 +1,9 @@
from django.contrib import admin
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
@ -17,14 +19,21 @@ STATE_COLORS = {
class TransactionAdmin(admin.ModelAdmin):
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')
account_link = admin_link('bill__account')
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)

View File

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

View File

@ -1,15 +1,52 @@
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 .forms import BankTransferForm, CreditCardForm
class PaymentMethod(plugins.Plugin):
label_field = 'label'
number_field = 'number'
__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):
verbose_name = _("Bank transfer")
label_field = 'name'
number_field = 'iban'
form = BankTransferForm
serializer = BankTransferSerializer
class CreditCard(PaymentMethod):
verbose_name = _("Credit card")
form = CreditCardForm
serializer = CreditCardSerializer

View File

@ -1,7 +1,11 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField
from orchestra.core import accounts
from . import settings
from .methods import PaymentMethod
@ -13,6 +17,25 @@ class PaymentSource(models.Model):
choices=PaymentMethod.get_plugin_choices())
data = JSONField(_("data"))
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):
@ -34,8 +57,8 @@ class Transaction(models.Model):
# TODO account fk?
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"),
related_name='transactions')
method = models.CharField(_("payment method"), max_length=32,
choices=PaymentMethod.get_plugin_choices())
source = models.ForeignKey(PaymentSource, verbose_name=_("source"),
related_name='transactions')
state = models.CharField(_("state"), max_length=32, choices=STATES,
default=WAITTING_PROCESSING)
data = JSONField(_("data"))
@ -47,3 +70,7 @@ class Transaction(models.Model):
def __unicode__(self):
return "Transaction {}".format(self.id)
accounts.register(PaymentSource)
accounts.register(Transaction)

View File

@ -1,14 +1,42 @@
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:
model = PaymentSource
class TransactionSerializer(serializers.HyperlinkedModelSerializer):
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(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
class Meta:
model = PaymentSource
model = Transaction

View File

@ -2,7 +2,7 @@ from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
from orchestra.core import services
from orchestra.core import accounts
from . import settings
@ -32,4 +32,4 @@ class Rate(models.Model):
return "{}-{}".format(str(self.value), self.quantity)
services.register(Pack, menu=False)
accounts.register(Pack)

View File

@ -25,10 +25,11 @@ class ResourceSerializer(serializers.ModelSerializer):
if not running_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'):
model = ct.model_class()
try:
router.insert(model, 'resources', ResourceSerializer, required=False, many=True)
router.insert(model, 'resources', ResourceSerializer, required=False, many=True, source='resource_set')
except KeyError:
continue

View File

@ -21,7 +21,7 @@ class SystemUserBackend(ServiceController):
if [[ $( id %(username)s ) ]]; then
usermod --password '%(password)s' %(username)s
else
useradd %(username)s --password '%(password)s' \
useradd %(username)s --password '%(password)s' \\
--shell /bin/false
fi
mkdir -p %(home)s
@ -74,8 +74,8 @@ class FTPTraffic(ServiceMonitor):
INI_DATE=$2
USERNAME="$3"
LOG_FILE="$4"
grep "UPLOAD\|DOWNLOAD" "${LOG_FILE}" \
| grep " \\[${USERNAME}\\] " \
grep "UPLOAD\|DOWNLOAD" "${LOG_FILE}" \\
| grep " \\[${USERNAME}\\] " \\
| awk -v ini="${INI_DATE}" '
BEGIN {
end = "%s"

View File

@ -24,20 +24,20 @@ class Apache2Backend(ServiceController):
extra_conf += self.get_security(site)
context['extra_conf'] = extra_conf
apache_conf = Template(
"# {{ banner }}\n"
"<VirtualHost *:{{ site.port }}>\n"
" ServerName {{ site.domains.all|first }}\n"
"{% if site.domains.all|slice:\"1:\" %}"
" ServerAlias {{ site.domains.all|slice:\"1:\"|join:' ' }}\n"
"{% endif %}"
" CustomLog {{ logs }} common\n"
" SuexecUserGroup {{ user }} {{ group }}\n"
"{% for line in extra_conf.splitlines %}"
" {{ line | safe }}\n"
"{% endfor %}"
"</VirtualHost>\n"
)
apache_conf = Template(textwrap.dedent("""\
# {{ banner }}
<VirtualHost *:{{ site.port }}>
ServerName {{ site.domains.all|first }}
{% if site.domains.all|slice:"1:" %}
ServerAlias {{ site.domains.all|slice:"1:"|join:' ' }}
{% endif %}
CustomLog {{ logs }} common
SuexecUserGroup {{ user }} {{ group }}
{% for line in extra_conf.splitlines %}"
{{ line | safe }}
{% endfor %}
</VirtualHost>"""
))
apache_conf = apache_conf.render(Context(context))
apache_conf += self.get_protections(site)
context['apache_conf'] = apache_conf

View File

@ -144,6 +144,7 @@ FLUENT_DASHBOARD_APP_GROUPS = (
'orchestra.apps.orders.models.Order',
'orchestra.apps.prices.models.Pack',
'orchestra.apps.bills.models.Bill',
# 'orchestra.apps.payments.models.PaymentSource',
'orchestra.apps.payments.models.Transaction',
'orchestra.apps.issues.models.Ticket',
),
@ -184,6 +185,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
'orders/service': 'price.png',
'prices/pack': 'Pack.png',
'bills/bill': 'invoice.png',
'payments/paymentsource': 'card_in_use.png',
'payments/transaction': 'transaction.png',
'issues/ticket': 'Ticket_star.png',
# Administration

View File

@ -1,5 +1,6 @@
class Service(object):
_registry = {}
class Register(object):
def __init__(self):
self._registry = {}
def __contains__(self, key):
return key in self._registry
@ -18,4 +19,6 @@ class Service(object):
return self._registry
services = Service()
services = Register()
# TODO rename to something else
accounts = Register()

View File

@ -1,38 +1,21 @@
def group_by(qset, *fields):
def group_by(qset, *fields, **kwargs):
""" group_by iterator with support for multiple nested fields """
def nest(objects, ix):
objs = []
result = []
first = True
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 = []
ix = kwargs.get('ix', 0)
if ix is 0:
qset = qset.order_by(*fields)
group = []
first = True
for obj in qset.order_by(*fields):
current = getattr(obj, fields[0])
for obj in qset:
current = getattr(obj, fields[ix])
if first or current == previous:
objs.append(obj)
group.append(obj)
else:
if len(fields) > 1:
objs = nest(objs, 1)
yield previous, objs
objs = [obj]
if ix < len(fields)-1:
group = group_by(group, *fields, ix=ix+1)
yield previous, group
group = [obj]
previous = current
first = False
if len(fields) > 1:
objs = nest(objs, 1)
yield current, objs
if ix < len(fields)-1:
group = group_by(group, *fields, ix=ix+1)
yield previous, group

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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