Refactor payment methods plugability
This commit is contained in:
parent
1f00b27667
commit
13df742284
2
TODO.md
2
TODO.md
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')]
|
||||
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:
|
||||
|
|
126
orchestra/apps/bills/migrations/0001_initial.py
Normal file
126
orchestra/apps/bills/migrations/0001_initial.py
Normal 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',),
|
||||
),
|
||||
]
|
21
orchestra/apps/bills/migrations/0002_bill_payment_source.py
Normal file
21
orchestra/apps/bills/migrations/0002_bill_payment_source.py
Normal 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,
|
||||
),
|
||||
]
|
0
orchestra/apps/bills/migrations/__init__.py
Normal file
0
orchestra/apps/bills/migrations/__init__.py
Normal 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):
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
{% if subject %}Bill {{ bill.number }}{% endif %}
|
||||
{% if message %}Find attached your invoice{% endif %}
|
|
@ -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)
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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')
|
||||
)
|
||||
|
|
|
@ -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"))
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' app_label='orders' %}">Slices</a>
|
||||
› <a href="{% url 'admin:orders_order_changelist' %}">Slices</a>
|
||||
› <a href="{% url 'admin:app_list' app_label='orders' %}">Orders</a>
|
||||
› <a href="{% url 'admin:orders_order_changelist' %}">Order</a>
|
||||
› {{ title }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue