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.
* 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.css_class = getattr(action, 'css_class', 'historylink')
view.description = getattr(action, 'description', '')
view.__name__ = action.__name__
views.append(view)
return views

View file

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

View file

@ -142,6 +142,12 @@ class AccountAdminMixin(object):
account_link.allow_tags = True
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):
""" Select related for performance """
qs = super(AccountAdminMixin, self).get_queryset(request)
@ -211,11 +217,6 @@ class AccountAdminMixin(object):
class SelectAccountAdminMixin(AccountAdminMixin):
""" 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):
inlines = super(AccountAdminMixin, self).get_inline_instances(request, obj=obj)
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 orchestra.core import services
from orchestra.utils import send_email_template
from . import settings
@ -31,5 +32,11 @@ class Account(models.Model):
def get_main(cls):
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)

View file

@ -7,20 +7,13 @@ from django.utils.translation import ugettext_lazy as _
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):
if queryset.count() > 1:
stringio = StringIO.StringIO()
archive = zipfile.ZipFile(stringio, 'w')
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.close()
response = HttpResponse(stringio.getvalue(), content_type='application/pdf')
@ -35,14 +28,22 @@ download_bills.url_name = 'download'
def view_bill(modeladmin, request, queryset):
bill = queryset.get()
bill.html = bill.render()
return HttpResponse(bill.html)
html = bill.html or bill.render()
return HttpResponse(html)
view_bill.verbose_name = _("View")
view_bill.url_name = 'view'
def close_bills(modeladmin, request, queryset):
# TODO confirmation with payment source selection
for bill in queryset:
bill.close()
close_bills.verbose_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 . 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 .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget,
BillLine, BudgetLine)
@ -51,6 +51,7 @@ class BudgetLineInline(admin.TabularInline):
fields = ('description', 'rate', 'amount', 'tax', 'total')
# TODO hide raw when status = oPen
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = (
'number', 'status', 'type_link', 'account_link', 'created_on_display',
@ -68,8 +69,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'fields': ('html',),
}),
)
actions = [render_bills, download_bills, close_bills]
change_view_actions = [render_bills, view_bill, download_bills]
actions = [download_bills, close_bills, send_bills]
change_view_actions = [view_bill, download_bills, send_bills, close_bills]
change_readonly_fields = ('account_link', 'type', 'status')
readonly_fields = ('number', 'display_total')
inlines = [BillLineInline]
@ -82,7 +83,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
num_lines.short_description = _("lines")
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.short_description = _("total")
@ -102,10 +103,15 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
def get_change_view_actions(self, obj=None):
actions = super(BillAdmin, self).get_change_view_actions(obj)
if obj and not obj.html:
actions = [action for action in actions
if action.__name__ not in ('view_bill', 'download_bills')]
return actions
discard = []
if obj:
if obj.status != Bill.OPEN:
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):
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.core import accounts
from orchestra.utils.functional import cached
from orchestra.utils.html import html_to_pdf
from . import settings
@ -25,16 +26,16 @@ class BillManager(models.Manager):
class Bill(models.Model):
OPEN = 'OPEN'
CLOSED = 'CLOSED'
SEND = 'SEND'
RETURNED = 'RETURNED'
SENT = 'SENT'
PAID = 'PAID'
RETURNED = 'RETURNED'
BAD_DEBT = 'BAD_DEBT'
STATUSES = (
(OPEN, _("Open")),
(CLOSED, _("Closed")),
(SEND, _("Sent")),
(RETURNED, _("Returned")),
(SENT, _("Sent")),
(PAID, _("Paid")),
(RETURNED, _("Returned")),
(BAD_DEBT, _("Bad debt")),
)
@ -50,6 +51,9 @@ class Bill(models.Model):
blank=True)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
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)
status = models.CharField(_("status"), max_length=16, choices=STATUSES,
default=OPEN)
@ -111,8 +115,26 @@ class Bill(models.Model):
prefix=prefix, year=year, number=number)
def close(self):
self.status = self.CLOSED
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()
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_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
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):
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"),
related_name='contacts', null=True)
short_name = models.CharField(_("short name"), max_length=128)
full_name = models.CharField(_("full name"), max_length=256, blank=True)
email = models.EmailField()
email_usage = MultiSelectField(_("email usage"), max_length=256, blank=True,
choices=settings.CONTACTS_EMAIL_USAGES,
choices=EMAIL_USAGES,
default=settings.CONTACTS_DEFAULT_EMAIL_USAGES)
phone = models.CharField(_("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.apps.accounts.serializers import AccountSerializerMixin
from . import settings
from .models import Contact, InvoiceContact
class ContactSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
email_usage = MultiSelectField(choices=settings.CONTACTS_EMAIL_USAGES)
email_usage = MultiSelectField(choices=Contact.EMAIL_USAGES)
class Meta:
model = Contact
fields = (

View file

@ -2,16 +2,6 @@ from django.conf import settings
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',
('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 orchestra.apps.contacts import settings as contacts_settings
from orchestra.apps.contacts.models import Contact
from orchestra.models.fields import MultiSelectField
from orchestra.utils import send_email_template
@ -14,7 +15,7 @@ class Queue(models.Model):
name = models.CharField(_("name"), max_length=128, unique=True)
default = models.BooleanField(_("default"), default=False)
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,
help_text=_("Contacts to notify by email"))

View file

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

View file

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

View file

@ -1,3 +1,4 @@
from django import forms
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
@ -12,7 +13,7 @@ from .models import PaymentSource, Transaction, PaymentProcess
STATE_COLORS = {
Transaction.WAITTING_PROCESSING: 'darkorange',
Transaction.WAITTING_CONFIRMATION: 'orange',
Transaction.WAITTING_CONFIRMATION: 'purple',
Transaction.CONFIRMED: 'green',
Transaction.REJECTED: 'red',
Transaction.LOCKED: 'magenta',
@ -20,14 +21,16 @@ STATE_COLORS = {
}
class TransactionAdmin(admin.ModelAdmin):
class TransactionAdmin(AccountAdminMixin, admin.ModelAdmin):
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')
actions = (process_transactions,)
filter_by_account_fields = ['source']
bill_link = admin_link('bill')
source_link = admin_link('source')
account_link = admin_link('bill__account')
display_state = admin_colored('state', colors=STATE_COLORS)
@ -39,8 +42,13 @@ class TransactionAdmin(admin.ModelAdmin):
class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ('label', 'method', 'number', 'account_link', '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):

View file

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

View file

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

View file

@ -6,7 +6,7 @@ from django.template.loader import render_to_string
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:
{% if subject %}Subject{% endif %}
@ -32,7 +32,7 @@ def send_email_template(template, context, to, email_from=None, html=None):
#subject cannot have new lines
subject = render_to_string(template, {'subject': True}, context).strip()
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:
html_message = render_to_string(html, {'message': True}, context)
msg.attach_alternative(html_message, "text/html")