Refactor payment methods plugability

This commit is contained in:
Marc 2014-09-04 15:55:43 +00:00
parent 1f00b27667
commit 13df742284
24 changed files with 340 additions and 82 deletions

View file

@ -78,4 +78,6 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
* make account_link to autoreplace account on change view. * make account_link to autoreplace account on change view.
* LAST version of this shit http://wkhtmltopdf.org/downloads.html * LAST version of this shit http://wkhtmltopdf.org/downloads.html
* Rename pack to plan ? one can have multiple plans?
* transaction.process FK?

View file

@ -73,7 +73,6 @@ class ChangeViewActionsMixin(object):
view.url_name.capitalize().replace('_', ' ')) view.url_name.capitalize().replace('_', ' '))
view.css_class = getattr(action, 'css_class', 'historylink') view.css_class = getattr(action, 'css_class', 'historylink')
view.description = getattr(action, 'description', '') view.description = getattr(action, 'description', '')
view.__name__ = action.__name__
views.append(view) views.append(view)
return views return views

View file

@ -1,4 +1,4 @@
from functools import update_wrapper from functools import wraps
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
@ -59,9 +59,10 @@ def insertattr(model, name, value, weight=0):
def wrap_admin_view(modeladmin, view): def wrap_admin_view(modeladmin, view):
""" Add admin authentication to view """ """ Add admin authentication to view """
@wraps(view)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
return modeladmin.admin_site.admin_view(view)(*args, **kwargs) return modeladmin.admin_site.admin_view(view)(*args, **kwargs)
return update_wrapper(wrapper, view) return wrapper
def set_url_query(request, key, value): def set_url_query(request, key, value):
@ -77,6 +78,7 @@ def set_url_query(request, key, value):
def action_to_view(action, modeladmin): def action_to_view(action, modeladmin):
""" Converts modeladmin action to view function """ """ Converts modeladmin action to view function """
@wraps(action)
def action_view(request, object_id=1, modeladmin=modeladmin, action=action): def action_view(request, object_id=1, modeladmin=modeladmin, action=action):
queryset = modeladmin.model.objects.filter(pk=object_id) queryset = modeladmin.model.objects.filter(pk=object_id)
response = action(modeladmin, request, queryset) response = action(modeladmin, request, queryset)

View file

@ -142,6 +142,12 @@ class AccountAdminMixin(object):
account_link.allow_tags = True account_link.allow_tags = True
account_link.admin_order_field = 'account__user__username' account_link.admin_order_field = 'account__user__username'
def get_readonly_fields(self, request, obj=None):
""" provide account for filter_by_account_fields """
if obj:
self.account = obj.account
return super(AccountAdminMixin, self).get_readonly_fields(request, obj=obj)
def get_queryset(self, request): def get_queryset(self, request):
""" Select related for performance """ """ Select related for performance """
qs = super(AccountAdminMixin, self).get_queryset(request) qs = super(AccountAdminMixin, self).get_queryset(request)
@ -211,11 +217,6 @@ class AccountAdminMixin(object):
class SelectAccountAdminMixin(AccountAdminMixin): class SelectAccountAdminMixin(AccountAdminMixin):
""" Provides support for accounts on ModelAdmin """ """ Provides support for accounts on ModelAdmin """
def get_readonly_fields(self, request, obj=None):
if obj:
self.account = obj.account
return super(AccountAdminMixin, self).get_readonly_fields(request, obj=obj)
def get_inline_instances(self, request, obj=None): def get_inline_instances(self, request, obj=None):
inlines = super(AccountAdminMixin, self).get_inline_instances(request, obj=obj) inlines = super(AccountAdminMixin, self).get_inline_instances(request, obj=obj)
if hasattr(self, 'account'): if hasattr(self, 'account'):

View file

@ -4,6 +4,7 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import services from orchestra.core import services
from orchestra.utils import send_email_template
from . import settings from . import settings
@ -30,6 +31,12 @@ class Account(models.Model):
@classmethod @classmethod
def get_main(cls): def get_main(cls):
return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK) return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK)
def send_email(self, template, context, contacts=[], attachments=[], html=None):
contacts = self.contacts.filter(email_usages=contacts)
email_to = contacts.values_list('email', flat=True)
send_email_template(template, context, email_to, html=html,
attachments=attachments)
services.register(Account, menu=False) services.register(Account, menu=False)

View file

@ -7,20 +7,13 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.utils.html import html_to_pdf from orchestra.utils.html import html_to_pdf
def render_bills(modeladmin, request, queryset):
for bill in queryset:
bill.html = bill.render()
bill.save()
render_bills.verbose_name = _("Render")
render_bills.url_name = 'render'
def download_bills(modeladmin, request, queryset): def download_bills(modeladmin, request, queryset):
if queryset.count() > 1: if queryset.count() > 1:
stringio = StringIO.StringIO() stringio = StringIO.StringIO()
archive = zipfile.ZipFile(stringio, 'w') archive = zipfile.ZipFile(stringio, 'w')
for bill in queryset: for bill in queryset:
pdf = html_to_pdf(bill.html) html = bill.html or bill.render()
pdf = html_to_pdf(html)
archive.writestr('%s.pdf' % bill.number, pdf) archive.writestr('%s.pdf' % bill.number, pdf)
archive.close() archive.close()
response = HttpResponse(stringio.getvalue(), content_type='application/pdf') response = HttpResponse(stringio.getvalue(), content_type='application/pdf')
@ -35,14 +28,22 @@ download_bills.url_name = 'download'
def view_bill(modeladmin, request, queryset): def view_bill(modeladmin, request, queryset):
bill = queryset.get() bill = queryset.get()
bill.html = bill.render() html = bill.html or bill.render()
return HttpResponse(bill.html) return HttpResponse(html)
view_bill.verbose_name = _("View") view_bill.verbose_name = _("View")
view_bill.url_name = 'view' view_bill.url_name = 'view'
def close_bills(modeladmin, request, queryset): def close_bills(modeladmin, request, queryset):
# TODO confirmation with payment source selection
for bill in queryset: for bill in queryset:
bill.close() bill.close()
close_bills.verbose_name = _("Close") close_bills.verbose_name = _("Close")
close_bills.url_name = 'close' close_bills.url_name = 'close'
def send_bills(modeladmin, request, queryset):
for bill in queryset:
bill.send()
send_bills.verbose_name = _("Send")
send_bills.url_name = 'send'

View file

@ -11,7 +11,7 @@ from orchestra.admin.utils import admin_link, admin_date
from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdminMixin
from . import settings from . import settings
from .actions import render_bills, download_bills, view_bill, close_bills from .actions import download_bills, view_bill, close_bills, send_bills
from .filters import BillTypeListFilter from .filters import BillTypeListFilter
from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget, from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget,
BillLine, BudgetLine) BillLine, BudgetLine)
@ -51,6 +51,7 @@ class BudgetLineInline(admin.TabularInline):
fields = ('description', 'rate', 'amount', 'tax', 'total') fields = ('description', 'rate', 'amount', 'tax', 'total')
# TODO hide raw when status = oPen
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = ( list_display = (
'number', 'status', 'type_link', 'account_link', 'created_on_display', 'number', 'status', 'type_link', 'account_link', 'created_on_display',
@ -68,8 +69,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'fields': ('html',), 'fields': ('html',),
}), }),
) )
actions = [render_bills, download_bills, close_bills] actions = [download_bills, close_bills, send_bills]
change_view_actions = [render_bills, view_bill, download_bills] change_view_actions = [view_bill, download_bills, send_bills, close_bills]
change_readonly_fields = ('account_link', 'type', 'status') change_readonly_fields = ('account_link', 'type', 'status')
readonly_fields = ('number', 'display_total') readonly_fields = ('number', 'display_total')
inlines = [BillLineInline] inlines = [BillLineInline]
@ -82,7 +83,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
num_lines.short_description = _("lines") num_lines.short_description = _("lines")
def display_total(self, bill): def display_total(self, bill):
return "%i &%s;" % (bill.get_total(), settings.BILLS_CURRENCY.lower()) return "%s &%s;" % (bill.get_total(), settings.BILLS_CURRENCY.lower())
display_total.allow_tags = True display_total.allow_tags = True
display_total.short_description = _("total") display_total.short_description = _("total")
@ -102,10 +103,15 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
def get_change_view_actions(self, obj=None): def get_change_view_actions(self, obj=None):
actions = super(BillAdmin, self).get_change_view_actions(obj) actions = super(BillAdmin, self).get_change_view_actions(obj)
if obj and not obj.html: discard = []
actions = [action for action in actions if obj:
if action.__name__ not in ('view_bill', 'download_bills')] if obj.status != Bill.OPEN:
return actions discard = ['close_bills']
if obj.status != Bill.CLOSED:
discard = ['send_bills']
if not discard:
return actions
return [action for action in actions if action.__name__ not in discard]
def get_inline_instances(self, request, obj=None): def get_inline_instances(self, request, obj=None):
if self.model is Budget: if self.model is Budget:

View file

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '__first__'),
]
operations = [
migrations.CreateModel(
name='Bill',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('number', models.CharField(unique=True, max_length=16, verbose_name='number', blank=True)),
('type', models.CharField(max_length=16, verbose_name='type', choices=[(b'INVOICE', 'Invoice'), (b'AMENDMENTINVOICE', 'Amendment invoice'), (b'FEE', 'Fee'), (b'AMENDMENTFEE', 'Amendment Fee'), (b'BUDGET', 'Budget')])),
('status', models.CharField(default=b'OPEN', max_length=16, verbose_name='status', choices=[(b'OPEN', 'Open'), (b'CLOSED', 'Closed'), (b'SENT', 'Sent'), (b'PAID', 'Paid'), (b'RETURNED', 'Returned'), (b'BAD_DEBT', 'Bad debt')])),
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
('due_on', models.DateField(null=True, verbose_name='due on', blank=True)),
('last_modified_on', models.DateTimeField(auto_now=True, verbose_name='last modified on')),
('comments', models.TextField(verbose_name='comments', blank=True)),
('html', models.TextField(verbose_name='HTML', blank=True)),
('account', models.ForeignKey(related_name=b'bill', verbose_name='account', to='accounts.Account')),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='BillLine',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('description', models.CharField(max_length=256, verbose_name='description')),
('rate', models.DecimalField(null=True, verbose_name='rate', max_digits=12, decimal_places=2, blank=True)),
('amount', models.DecimalField(verbose_name='amount', max_digits=12, decimal_places=2)),
('total', models.DecimalField(verbose_name='total', max_digits=12, decimal_places=2)),
('tax', models.PositiveIntegerField(verbose_name='tax')),
('order_id', models.PositiveIntegerField(null=True, blank=True)),
('order_last_bill_date', models.DateTimeField(null=True)),
('order_billed_until', models.DateTimeField(null=True)),
('auto', models.BooleanField(default=False)),
('amended_line', models.ForeignKey(related_name=b'amendment_lines', verbose_name='amended line', blank=True, to='bills.BillLine', null=True)),
('bill', models.ForeignKey(related_name=b'billlines', verbose_name='bill', to='bills.Bill')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='BillSubline',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('description', models.CharField(max_length=256, verbose_name='description')),
('total', models.DecimalField(max_digits=12, decimal_places=2)),
('bill_line', models.ForeignKey(related_name=b'sublines', verbose_name='bill line', to='bills.BillLine')),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='BudgetLine',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('description', models.CharField(max_length=256, verbose_name='description')),
('rate', models.DecimalField(null=True, verbose_name='rate', max_digits=12, decimal_places=2, blank=True)),
('amount', models.DecimalField(verbose_name='amount', max_digits=12, decimal_places=2)),
('total', models.DecimalField(verbose_name='total', max_digits=12, decimal_places=2)),
('tax', models.PositiveIntegerField(verbose_name='tax')),
('bill', models.ForeignKey(related_name=b'budgetlines', verbose_name='bill', to='bills.Bill')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='AmendmentFee',
fields=[
],
options={
'proxy': True,
},
bases=('bills.bill',),
),
migrations.CreateModel(
name='AmendmentInvoice',
fields=[
],
options={
'proxy': True,
},
bases=('bills.bill',),
),
migrations.CreateModel(
name='Budget',
fields=[
],
options={
'proxy': True,
},
bases=('bills.bill',),
),
migrations.CreateModel(
name='Fee',
fields=[
],
options={
'proxy': True,
},
bases=('bills.bill',),
),
migrations.CreateModel(
name='Invoice',
fields=[
],
options={
'proxy': True,
},
bases=('bills.bill',),
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('payments', '__first__'),
('bills', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='bill',
name='payment_source',
field=models.ForeignKey(blank=True, to='payments.PaymentSource', help_text='Optionally specify a payment source for this bill', null=True, verbose_name='payment source'),
preserve_default=True,
),
]

View file

@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.accounts.models import Account from orchestra.apps.accounts.models import Account
from orchestra.core import accounts from orchestra.core import accounts
from orchestra.utils.functional import cached from orchestra.utils.functional import cached
from orchestra.utils.html import html_to_pdf
from . import settings from . import settings
@ -25,16 +26,16 @@ class BillManager(models.Manager):
class Bill(models.Model): class Bill(models.Model):
OPEN = 'OPEN' OPEN = 'OPEN'
CLOSED = 'CLOSED' CLOSED = 'CLOSED'
SEND = 'SEND' SENT = 'SENT'
RETURNED = 'RETURNED'
PAID = 'PAID' PAID = 'PAID'
RETURNED = 'RETURNED'
BAD_DEBT = 'BAD_DEBT' BAD_DEBT = 'BAD_DEBT'
STATUSES = ( STATUSES = (
(OPEN, _("Open")), (OPEN, _("Open")),
(CLOSED, _("Closed")), (CLOSED, _("Closed")),
(SEND, _("Sent")), (SENT, _("Sent")),
(RETURNED, _("Returned")),
(PAID, _("Paid")), (PAID, _("Paid")),
(RETURNED, _("Returned")),
(BAD_DEBT, _("Bad debt")), (BAD_DEBT, _("Bad debt")),
) )
@ -50,6 +51,9 @@ class Bill(models.Model):
blank=True) blank=True)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='%(class)s') related_name='%(class)s')
payment_source = models.ForeignKey('payments.PaymentSource', null=True,
verbose_name=_("payment source"),
help_text=_("Optionally specify a payment source for this bill"))
type = models.CharField(_("type"), max_length=16, choices=TYPES) type = models.CharField(_("type"), max_length=16, choices=TYPES)
status = models.CharField(_("status"), max_length=16, choices=STATUSES, status = models.CharField(_("status"), max_length=16, choices=STATUSES,
default=OPEN) default=OPEN)
@ -111,8 +115,26 @@ class Bill(models.Model):
prefix=prefix, year=year, number=number) prefix=prefix, year=year, number=number)
def close(self): def close(self):
self.status = self.CLOSED
self.html = self.render() self.html = self.render()
self.status = self.CLOSED
self.save()
def send(self):
from orchestra.apps.contacts.models import Contact
self.account.send_email(
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
context={
'bill': self,
},
contacts=(Contact.BILLING,),
attachments=[
('%s.pdf' % self.number, html_to_pdf(self.html), 'application/pdf')
]
)
self.transactions.create(
bill=self, source=self.payment_source, amount=self.get_total()
)
self.status = self.SENT
self.save() self.save()
def render(self): def render(self):

View file

@ -29,3 +29,8 @@ BILLS_SELLER_PHONE = getattr(settings, 'BILLS_SELLER_PHONE', '111-112-11-222')
BILLS_SELLER_EMAIL = getattr(settings, 'BILLS_SELLER_EMAIL', 'sales@orchestra.lan') BILLS_SELLER_EMAIL = getattr(settings, 'BILLS_SELLER_EMAIL', 'sales@orchestra.lan')
BILLS_SELLER_WEBSITE = getattr(settings, 'BILLS_SELLER_WEBSITE', 'www.orchestra.lan') BILLS_SELLER_WEBSITE = getattr(settings, 'BILLS_SELLER_WEBSITE', 'www.orchestra.lan')
BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE',
'bills/bill-notification.email')

View file

@ -0,0 +1,34 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n staticfiles admin_urls %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
{% endblock %}
{% block breadcrumbs %}
TODO
{% endblock %}
{% block content %}
<h1>Are you sure you want to close selected bills</h1>
<p>Once a bill is closed it can not be further modified.</p>
<p>Please select a payment source for the selected bills </p>
<form action="" method="post">{% csrf_token %}
<div>
<div style="margin:20px;">
{{ form.as_admin }}
</div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
<input type="hidden" name="action" value="close_selected_bills"/>
<input type="submit" value="{% trans "Yes, close bills" %}" />
</div>
</form>
{% endblock %}

View file

@ -0,0 +1,2 @@
{% if subject %}Bill {{ bill.number }}{% endif %}
{% if message %}Find attached your invoice{% endif %}

View file

@ -7,14 +7,35 @@ from orchestra.models.fields import MultiSelectField
from . import settings from . import settings
class ContactQuerySet(models.QuerySet):
def filter(self, *args, **kwargs):
usages = kwargs.pop('email_usages', [])
qs = models.Q()
for usage in usages:
qs = qs | models.Q(email_usage__regex=r'.*(^|,)+%s($|,)+.*' % usage)
return super(ContactQuerySet, self).filter(qs, *args, **kwargs)
class Contact(models.Model): class Contact(models.Model):
BILLING = 'BILLING'
EMAIL_USAGES = (
('SUPPORT', _("Support tickets")),
('ADMIN', _("Administrative")),
(BILLING, _("Billing")),
('TECH', _("Technical")),
('ADDS', _("Announcements")),
('EMERGENCY', _("Emergency contact")),
)
objects = ContactQuerySet.as_manager()
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='contacts', null=True) related_name='contacts', null=True)
short_name = models.CharField(_("short name"), max_length=128) short_name = models.CharField(_("short name"), max_length=128)
full_name = models.CharField(_("full name"), max_length=256, blank=True) full_name = models.CharField(_("full name"), max_length=256, blank=True)
email = models.EmailField() email = models.EmailField()
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=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)

View file

@ -3,12 +3,11 @@ from rest_framework import serializers
from orchestra.api.serializers import MultiSelectField from orchestra.api.serializers import MultiSelectField
from orchestra.apps.accounts.serializers import AccountSerializerMixin from orchestra.apps.accounts.serializers import AccountSerializerMixin
from . import settings
from .models import Contact, InvoiceContact from .models import Contact, InvoiceContact
class ContactSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class ContactSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
email_usage = MultiSelectField(choices=settings.CONTACTS_EMAIL_USAGES) email_usage = MultiSelectField(choices=Contact.EMAIL_USAGES)
class Meta: class Meta:
model = Contact model = Contact
fields = ( fields = (

View file

@ -2,16 +2,6 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
CONTACTS_EMAIL_USAGES = getattr(settings, 'CONTACTS_EMAIL_USAGES', (
('SUPPORT', _("Support tickets")),
('ADMIN', _("Administrative")),
('BILL', _("Billing")),
('TECH', _("Technical")),
('ADDS', _("Announcements")),
('EMERGENCY', _("Emergency contact")),
))
CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES', CONTACTS_DEFAULT_EMAIL_USAGES = getattr(settings, 'CONTACTS_DEFAULT_EMAIL_USAGES',
('SUPPORT', 'ADMIN', 'BILL', 'TECH', 'ADDS', 'EMERGENCY') ('SUPPORT', 'ADMIN', 'BILL', 'TECH', 'ADDS', 'EMERGENCY')
) )

View file

@ -4,6 +4,7 @@ from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.contacts import settings as contacts_settings from orchestra.apps.contacts import settings as contacts_settings
from orchestra.apps.contacts.models import Contact
from orchestra.models.fields import MultiSelectField from orchestra.models.fields import MultiSelectField
from orchestra.utils import send_email_template from orchestra.utils import send_email_template
@ -14,7 +15,7 @@ class Queue(models.Model):
name = models.CharField(_("name"), max_length=128, unique=True) name = models.CharField(_("name"), max_length=128, unique=True)
default = models.BooleanField(_("default"), default=False) default = models.BooleanField(_("default"), default=False)
notify = MultiSelectField(_("notify"), max_length=256, blank=True, notify = MultiSelectField(_("notify"), max_length=256, blank=True,
choices=contacts_settings.CONTACTS_EMAIL_USAGES, choices=Contact.EMAIL_USAGES,
default=contacts_settings.CONTACTS_DEFAULT_EMAIL_USAGES, default=contacts_settings.CONTACTS_DEFAULT_EMAIL_USAGES,
help_text=_("Contacts to notify by email")) help_text=_("Contacts to notify by email"))

View file

@ -9,8 +9,8 @@
{% block breadcrumbs %} {% block breadcrumbs %}
<div class="breadcrumbs"> <div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> <a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label='orders' %}">Slices</a> &rsaquo; <a href="{% url 'admin:app_list' app_label='orders' %}">Orders</a>
&rsaquo; <a href="{% url 'admin:orders_order_changelist' %}">Slices</a> &rsaquo; <a href="{% url 'admin:orders_order_changelist' %}">Order</a>
&rsaquo; {{ title }} &rsaquo; {{ title }}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,4 +1,4 @@
def process_transactions(modeladmin, request, queryset): def process_transactions(modeladmin, request, queryset):
from .methods import SEPADirectDebit for source, transactions in queryset.group_by('source'):
SEPADirectDebit().process(queryset) if source:
source.method_class().process(transactions)

View file

@ -1,3 +1,4 @@
from django import forms
from django.contrib import admin from django.contrib import admin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -12,7 +13,7 @@ from .models import PaymentSource, Transaction, PaymentProcess
STATE_COLORS = { STATE_COLORS = {
Transaction.WAITTING_PROCESSING: 'darkorange', Transaction.WAITTING_PROCESSING: 'darkorange',
Transaction.WAITTING_CONFIRMATION: 'orange', Transaction.WAITTING_CONFIRMATION: 'purple',
Transaction.CONFIRMED: 'green', Transaction.CONFIRMED: 'green',
Transaction.REJECTED: 'red', Transaction.REJECTED: 'red',
Transaction.LOCKED: 'magenta', Transaction.LOCKED: 'magenta',
@ -20,14 +21,16 @@ STATE_COLORS = {
} }
class TransactionAdmin(admin.ModelAdmin): class TransactionAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ( list_display = (
'id', 'bill_link', 'account_link', 'source', 'display_state', 'amount' 'id', 'bill_link', 'account_link', 'source_link', 'display_state', 'amount'
) )
list_filter = ('source__method', 'state') list_filter = ('source__method', 'state')
actions = (process_transactions,) actions = (process_transactions,)
filter_by_account_fields = ['source']
bill_link = admin_link('bill') bill_link = admin_link('bill')
source_link = admin_link('source')
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)
@ -39,8 +42,13 @@ class TransactionAdmin(admin.ModelAdmin):
class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin): class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ('label', 'method', 'number', 'account_link', 'is_active') list_display = ('label', 'method', 'number', 'account_link', 'is_active')
list_filter = ('method', 'is_active') list_filter = ('method', 'is_active')
form = SEPADirectDebit().get_form()
# TODO select payment source method def get_form(self, request, obj=None, **kwargs):
if obj:
self.form = obj.method_class().get_form()
else:
self.form = forms.ModelForm
return super(PaymentSourceAdmin, self).get_form(request, obj=obj, **kwargs)
class PaymentProcessAdmin(admin.ModelAdmin): class PaymentProcessAdmin(admin.ModelAdmin):

View file

@ -29,7 +29,7 @@ class SEPADirectDebitSerializer(serializers.Serializer):
class SEPADirectDebit(PaymentMethod): class SEPADirectDebit(PaymentMethod):
verbose_name = _("Direct Debit") verbose_name = _("SEPA Direct Debit")
label_field = 'name' label_field = 'name'
number_field = 'iban' number_field = 'iban'
process_credit = True process_credit = True
@ -154,10 +154,9 @@ class SEPADirectDebit(PaymentMethod):
def _get_debt_transactions(self, transactions): def _get_debt_transactions(self, transactions):
for transaction in transactions: for transaction in transactions:
self.object.transactions.add(transaction) self.object.transactions.add(transaction)
# TODO transaction.account account = transaction.account
account = transaction.bill.account # TODO
# FIXME data = account.paymentsources.first().data
data = account.payment_sources.first().data
transaction.state = transaction.WAITTING_CONFIRMATION transaction.state = transaction.WAITTING_CONFIRMATION
transaction.save() transaction.save()
yield E.DrctDbtTxInf( # Direct Debit Transaction Info yield E.DrctDbtTxInf( # Direct Debit Transaction Info
@ -196,8 +195,7 @@ class SEPADirectDebit(PaymentMethod):
def _get_credit_transactions(self, transactions): def _get_credit_transactions(self, transactions):
for transaction in transactions: for transaction in transactions:
self.object.transactions.add(transaction) self.object.transactions.add(transaction)
# TODO transaction.account account = transaction.account
account = transaction.bill.account
# FIXME # FIXME
data = account.payment_sources.first().data data = account.payment_sources.first().data
transaction.state = transaction.WAITTING_CONFIRMATION transaction.state = transaction.WAITTING_CONFIRMATION

View file

@ -5,37 +5,46 @@ from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField from jsonfield import JSONField
from orchestra.core import accounts from orchestra.core import accounts
from orchestra.models.queryset import group_by
from . import settings from . import settings
from .methods import PaymentMethod from .methods import PaymentMethod
class PaymentSourcesQueryset(models.QuerySet):
def get_source(self):
# TODO
return self.filter(is_active=True).first()
class PaymentSource(models.Model): class PaymentSource(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='payment_sources') related_name='paymentsources')
method = models.CharField(_("method"), max_length=32, method = models.CharField(_("method"), max_length=32,
choices=PaymentMethod.get_plugin_choices()) choices=PaymentMethod.get_plugin_choices())
data = JSONField(_("data")) data = JSONField(_("data"))
is_active = models.BooleanField(_("is active"), default=True) is_active = models.BooleanField(_("is active"), default=True)
objects = PaymentSourcesQueryset.as_manager()
def __unicode__(self): def __unicode__(self):
return self.label or str(self.account) return "%s (%s)" % (self.label, self.method_class.verbose_name)
@cached_property
def method_class(self):
return PaymentMethod.get_plugin(self.method)
@cached_property @cached_property
def label(self): def label(self):
try: return self.method_class().get_label(self.data)
plugin = PaymentMethod.get_plugin(self.method)()
except KeyError:
return None
return plugin.get_label(self.data)
@cached_property @cached_property
def number(self): def number(self):
try: return self.method_class().get_number(self.data)
plugin = PaymentMethod.get_plugin(self.method)()
except KeyError:
return None class TransactionQuerySet(models.QuerySet):
return plugin.get_number(self.data) group_by = group_by
# TODO lock transaction in waiting confirmation # TODO lock transaction in waiting confirmation
@ -55,7 +64,8 @@ class Transaction(models.Model):
(DISCARTED, _("Discarted")), (DISCARTED, _("Discarted")),
) )
# TODO account fk? objects = TransactionQuerySet.as_manager()
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"), bill = models.ForeignKey('bills.bill', verbose_name=_("bill"),
related_name='transactions') related_name='transactions')
source = models.ForeignKey(PaymentSource, null=True, blank=True, source = models.ForeignKey(PaymentSource, null=True, blank=True,
@ -66,10 +76,13 @@ class Transaction(models.Model):
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY) currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
created_on = models.DateTimeField(auto_now_add=True) created_on = models.DateTimeField(auto_now_add=True)
modified_on = models.DateTimeField(auto_now=True) modified_on = models.DateTimeField(auto_now=True)
related = models.ForeignKey('self', null=True, blank=True)
def __unicode__(self): def __unicode__(self):
return "Transaction {}".format(self.id) return "Transaction {}".format(self.id)
@property
def account(self):
return self.bill.account
# TODO rename to TransactionProcess or PaymentRequest TransactionRequest # TODO rename to TransactionProcess or PaymentRequest TransactionRequest

View file

@ -6,20 +6,20 @@ from django.template.loader import render_to_string
from django.template import Context from django.template import Context
def send_email_template(template, context, to, email_from=None, html=None): def send_email_template(template, context, to, email_from=None, html=None, attachments=[]):
""" """
Renders an email template with this format: Renders an email template with this format:
{% if subject %}Subject{% endif %} {% if subject %}Subject{% endif %}
{% if message %}Email body{% endif %} {% if message %}Email body{% endif %}
context can be a dictionary or a template.Context instance context can be a dictionary or a template.Context instance
""" """
if isinstance(context, dict): if isinstance(context, dict):
context = Context(context) context = Context(context)
if type(to) in [str, unicode]: if type(to) in [str, unicode]:
to = [to] to = [to]
if not 'site' in context: if not 'site' in context:
from orchestra import settings from orchestra import settings
url = urlparse.urlparse(settings.SITE_URL) url = urlparse.urlparse(settings.SITE_URL)
@ -32,7 +32,7 @@ def send_email_template(template, context, to, email_from=None, html=None):
#subject cannot have new lines #subject cannot have new lines
subject = render_to_string(template, {'subject': True}, context).strip() subject = render_to_string(template, {'subject': True}, context).strip()
message = render_to_string(template, {'message': True}, context) message = render_to_string(template, {'message': True}, context)
msg = EmailMultiAlternatives(subject, message, email_from, to) msg = EmailMultiAlternatives(subject, message, email_from, to, attachments=attachments)
if html: if html:
html_message = render_to_string(html, {'message': True}, context) html_message = render_to_string(html, {'message': True}, context)
msg.attach_alternative(html_message, "text/html") msg.attach_alternative(html_message, "text/html")