Random fixes
This commit is contained in:
parent
1c8ef622b0
commit
920f8efcd5
4
TODO.md
4
TODO.md
|
@ -90,7 +90,7 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
|
||||||
|
|
||||||
* mail backend related_models = ('resources__content_type') ??
|
* mail backend related_models = ('resources__content_type') ??
|
||||||
|
|
||||||
* ignore orders (mark orders as ignored)
|
* ignore orders (mark orders as ignored), ignore orchestra related orders by default (or do not generate them on the first place) ignore superuser orders?
|
||||||
|
|
||||||
* Domain backend PowerDNS Bind validation support?
|
* Domain backend PowerDNS Bind validation support?
|
||||||
|
|
||||||
|
@ -115,8 +115,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
|
||||||
|
|
||||||
* Separate panel from server passwords? Store passwords on panel? set_password special backend operation?
|
* Separate panel from server passwords? Store passwords on panel? set_password special backend operation?
|
||||||
|
|
||||||
* be more explicit about which backends are resources and which are service handling
|
|
||||||
|
|
||||||
* What fields we really need on contacts? name email phone and what more?
|
* What fields we really need on contacts? name email phone and what more?
|
||||||
|
|
||||||
* Redirect junk emails and delete every 30 days?
|
* Redirect junk emails and delete every 30 days?
|
||||||
|
|
|
@ -86,8 +86,11 @@ class ChangeViewActionsMixin(object):
|
||||||
action = getattr(self, action)
|
action = getattr(self, action)
|
||||||
view = action_to_view(action, self)
|
view = action_to_view(action, self)
|
||||||
view.url_name = getattr(action, 'url_name', action.__name__)
|
view.url_name = getattr(action, 'url_name', action.__name__)
|
||||||
view.verbose_name = getattr(action, 'verbose_name',
|
verbose_name = getattr(action, 'verbose_name',
|
||||||
view.url_name.capitalize().replace('_', ' '))
|
view.url_name.capitalize().replace('_', ' '))
|
||||||
|
if hasattr(verbose_name, '__call__'):
|
||||||
|
verbose_name = verbose_name(obj)
|
||||||
|
view.verbose_name = verbose_name
|
||||||
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', '')
|
||||||
views.append(view)
|
views.append(view)
|
||||||
|
@ -186,7 +189,7 @@ class SelectPluginAdminMixin(object):
|
||||||
def add_view(self, request, form_url='', extra_context=None):
|
def add_view(self, request, form_url='', extra_context=None):
|
||||||
""" Redirects to select account view if required """
|
""" Redirects to select account view if required """
|
||||||
if request.user.is_superuser:
|
if request.user.is_superuser:
|
||||||
plugin_value = request.GET.get(self.plugin_field)
|
plugin_value = request.GET.get(self.plugin_field) or request.POST.get(self.plugin_field)
|
||||||
if plugin_value or self.plugin.get_plugins() == 1:
|
if plugin_value or self.plugin.get_plugins() == 1:
|
||||||
self.plugin_value = plugin_value
|
self.plugin_value = plugin_value
|
||||||
if not plugin_value:
|
if not plugin_value:
|
||||||
|
|
|
@ -4,37 +4,17 @@ import zipfile
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.admin import helpers
|
from django.contrib.admin import helpers
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.http import HttpResponse, HttpResponseServerError
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.utils.encoding import force_text
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin.forms import adminmodelformset_factory
|
from orchestra.admin.forms import adminmodelformset_factory
|
||||||
from orchestra.admin.utils import get_object_from_url
|
from orchestra.admin.utils import get_object_from_url, change_url
|
||||||
from orchestra.utils.html import html_to_pdf
|
from orchestra.utils.html import html_to_pdf
|
||||||
|
|
||||||
from .forms import SelectSourceForm
|
from .forms import SelectSourceForm
|
||||||
|
from .helpers import validate_contact
|
||||||
def validate_contact(bill):
|
|
||||||
""" checks if all the preconditions for bill generation are met """
|
|
||||||
msg = ''
|
|
||||||
if not hasattr(bill.account, 'invoicecontact'):
|
|
||||||
account = force_text(bill.account)
|
|
||||||
link = reverse('admin:accounts_account_change', args=(bill.account_id,))
|
|
||||||
link += '#invoicecontact-group'
|
|
||||||
msg += _('Related account "%s" doesn\'t have a declared invoice contact\n') % account
|
|
||||||
msg += _('You should <a href="%s">provide</a> one') % link
|
|
||||||
main = type(bill).account.field.rel.to.get_main()
|
|
||||||
if not hasattr(main, 'invoicecontact'):
|
|
||||||
account = force_text(main)
|
|
||||||
link = reverse('admin:accounts_account_change', args=(main.id,))
|
|
||||||
link += '#invoicecontact-group'
|
|
||||||
msg += _('Main account "%s" doesn\'t have a declared invoice contact\n') % account
|
|
||||||
msg += _('You should <a href="%s">provide</a> one') % link
|
|
||||||
if msg:
|
|
||||||
# TODO custom template
|
|
||||||
return HttpResponseServerError(mark_safe(msg))
|
|
||||||
|
|
||||||
|
|
||||||
def download_bills(modeladmin, request, queryset):
|
def download_bills(modeladmin, request, queryset):
|
||||||
|
@ -42,15 +22,14 @@ def download_bills(modeladmin, request, queryset):
|
||||||
stringio = StringIO.StringIO()
|
stringio = StringIO.StringIO()
|
||||||
archive = zipfile.ZipFile(stringio, 'w')
|
archive = zipfile.ZipFile(stringio, 'w')
|
||||||
for bill in queryset:
|
for bill in queryset:
|
||||||
html = bill.html or bill.render()
|
pdf = html_to_pdf(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')
|
||||||
response['Content-Disposition'] = 'attachment; filename="orchestra-bills.zip"'
|
response['Content-Disposition'] = 'attachment; filename="orchestra-bills.zip"'
|
||||||
return response
|
return response
|
||||||
bill = queryset.get()
|
bill = queryset.get()
|
||||||
pdf = html_to_pdf(bill.html)
|
pdf = html_to_pdf(bill.html or bill.render())
|
||||||
return HttpResponse(pdf, content_type='application/pdf')
|
return HttpResponse(pdf, content_type='application/pdf')
|
||||||
download_bills.verbose_name = _("Download")
|
download_bills.verbose_name = _("Download")
|
||||||
download_bills.url_name = 'download'
|
download_bills.url_name = 'download'
|
||||||
|
@ -58,9 +37,8 @@ download_bills.url_name = 'download'
|
||||||
|
|
||||||
def view_bill(modeladmin, request, queryset):
|
def view_bill(modeladmin, request, queryset):
|
||||||
bill = queryset.get()
|
bill = queryset.get()
|
||||||
error = validate_contact(bill)
|
if not validate_contact(request, bill):
|
||||||
if error:
|
return
|
||||||
return error
|
|
||||||
html = bill.html or bill.render()
|
html = bill.html or bill.render()
|
||||||
return HttpResponse(html)
|
return HttpResponse(html)
|
||||||
view_bill.verbose_name = _("View")
|
view_bill.verbose_name = _("View")
|
||||||
|
@ -73,20 +51,34 @@ def close_bills(modeladmin, request, queryset):
|
||||||
messages.warning(request, _("Selected bills should be in open state"))
|
messages.warning(request, _("Selected bills should be in open state"))
|
||||||
return
|
return
|
||||||
for bill in queryset:
|
for bill in queryset:
|
||||||
error = validate_contact(bill)
|
if not validate_contact(request, bill):
|
||||||
if error:
|
return
|
||||||
return error
|
|
||||||
SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, extra=0)
|
SelectSourceFormSet = adminmodelformset_factory(modeladmin, SelectSourceForm, extra=0)
|
||||||
formset = SelectSourceFormSet(queryset=queryset)
|
formset = SelectSourceFormSet(queryset=queryset)
|
||||||
if request.POST.get('post') == 'generic_confirmation':
|
if request.POST.get('post') == 'generic_confirmation':
|
||||||
formset = SelectSourceFormSet(request.POST, request.FILES, queryset=queryset)
|
formset = SelectSourceFormSet(request.POST, request.FILES, queryset=queryset)
|
||||||
if formset.is_valid():
|
if formset.is_valid():
|
||||||
|
transactions = []
|
||||||
for form in formset.forms:
|
for form in formset.forms:
|
||||||
source = form.cleaned_data['source']
|
source = form.cleaned_data['source']
|
||||||
form.instance.close(payment=source)
|
transaction = form.instance.close(payment=source)
|
||||||
|
if transaction:
|
||||||
|
transactions.append(transaction)
|
||||||
for bill in queryset:
|
for bill in queryset:
|
||||||
modeladmin.log_change(request, bill, 'Closed')
|
modeladmin.log_change(request, bill, 'Closed')
|
||||||
messages.success(request, _("Selected bills have been closed"))
|
messages.success(request, _("Selected bills have been closed"))
|
||||||
|
if transactions:
|
||||||
|
num = len(transactions)
|
||||||
|
if num == 1:
|
||||||
|
url = change_url(transactions[0])
|
||||||
|
else:
|
||||||
|
url = reverse('admin:transactions_transaction_changelist')
|
||||||
|
url += 'id__in=%s' % ','.join([str(t.id) for t in transactions])
|
||||||
|
message = ungettext(
|
||||||
|
_('<a href="%s">One related transaction</a> has been created') % url,
|
||||||
|
_('<a href="%s">%i related transactions</a> have been created') % (url, num),
|
||||||
|
num)
|
||||||
|
messages.success(request, mark_safe(message))
|
||||||
return
|
return
|
||||||
opts = modeladmin.model._meta
|
opts = modeladmin.model._meta
|
||||||
context = {
|
context = {
|
||||||
|
@ -110,11 +102,10 @@ close_bills.url_name = 'close'
|
||||||
|
|
||||||
def send_bills(modeladmin, request, queryset):
|
def send_bills(modeladmin, request, queryset):
|
||||||
for bill in queryset:
|
for bill in queryset:
|
||||||
error = validate_contact(bill)
|
if not validate_contact(request, bill):
|
||||||
if error:
|
return
|
||||||
return error
|
|
||||||
for bill in queryset:
|
for bill in queryset:
|
||||||
bill.send()
|
bill.send()
|
||||||
modeladmin.log_change(request, bill, 'Sent')
|
modeladmin.log_change(request, bill, 'Sent')
|
||||||
send_bills.verbose_name = _("Send")
|
send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', False) else "Send")
|
||||||
send_bills.url_name = 'send'
|
send_bills.url_name = 'send'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -11,7 +11,7 @@ from orchestra.admin.utils import 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 download_bills, view_bill, close_bills, send_bills
|
from .actions import download_bills, view_bill, close_bills, send_bills, validate_contact
|
||||||
from .filters import BillTypeListFilter
|
from .filters import BillTypeListFilter
|
||||||
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine
|
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine
|
||||||
|
|
||||||
|
@ -55,9 +55,9 @@ class BillLineInline(admin.TabularInline):
|
||||||
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
'number', 'type_link', 'account_link', 'created_on_display',
|
'number', 'type_link', 'account_link', 'created_on_display',
|
||||||
'num_lines', 'display_total', 'display_payment_state', 'is_open'
|
'num_lines', 'display_total', 'display_payment_state', 'is_open', 'is_sent'
|
||||||
)
|
)
|
||||||
list_filter = (BillTypeListFilter, 'is_open',)
|
list_filter = (BillTypeListFilter, 'is_open', 'is_sent')
|
||||||
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
|
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {
|
(None, {
|
||||||
|
@ -97,8 +97,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
type_link.admin_order_field = 'type'
|
type_link.admin_order_field = 'type'
|
||||||
|
|
||||||
def display_payment_state(self, bill):
|
def display_payment_state(self, bill):
|
||||||
topts = bill.transactions.model._meta
|
t_opts = bill.transactions.model._meta
|
||||||
url = reverse('admin:%s_%s_changelist' % (topts.app_label, topts.module_name))
|
transactions = bill.transactions.all()
|
||||||
|
if len(transactions) == 1:
|
||||||
|
args = (transactions[0].pk,)
|
||||||
|
url = reverse('admin:%s_%s_change' % (t_opts.app_label, t_opts.module_name), args=args)
|
||||||
|
else:
|
||||||
|
url = reverse('admin:%s_%s_changelist' % (t_opts.app_label, t_opts.module_name))
|
||||||
url += '?bill=%i' % bill.pk
|
url += '?bill=%i' % bill.pk
|
||||||
state = bill.get_payment_state_display().upper()
|
state = bill.get_payment_state_display().upper()
|
||||||
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
||||||
|
@ -120,7 +125,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
return fieldsets
|
return fieldsets
|
||||||
|
|
||||||
def get_change_view_actions(self, obj=None):
|
def get_change_view_actions(self, obj=None):
|
||||||
actions = super(BillAdmin, self).get_change_view_actions()
|
actions = super(BillAdmin, self).get_change_view_actions(obj=obj)
|
||||||
exclude = []
|
exclude = []
|
||||||
if obj:
|
if obj:
|
||||||
if not obj.is_open:
|
if not obj.is_open:
|
||||||
|
@ -143,19 +148,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(BillAdmin, self).get_queryset(request)
|
qs = super(BillAdmin, self).get_queryset(request)
|
||||||
qs = qs.annotate(models.Count('lines'))
|
qs = qs.annotate(models.Count('lines'))
|
||||||
qs = qs.prefetch_related('lines', 'lines__sublines')
|
qs = qs.prefetch_related('lines', 'lines__sublines', 'transactions')
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def change_view(self, request, object_id, **kwargs):
|
def change_view(self, request, object_id, **kwargs):
|
||||||
bill = self.get_object(request, unquote(object_id))
|
|
||||||
# TODO raise404, here and everywhere
|
# TODO raise404, here and everywhere
|
||||||
if not hasattr(bill.account, 'invoicecontact'):
|
bill = self.get_object(request, unquote(object_id))
|
||||||
create_link = reverse('admin:accounts_account_change', args=(bill.account_id,))
|
validate_contact(request, bill, error=False)
|
||||||
create_link += '#invoicecontact-group'
|
|
||||||
messages.warning(request, mark_safe(_(
|
|
||||||
'Be aware, related contact doesn\'t have a billing contact defined, '
|
|
||||||
'bill can not be generated until one is <a href="%s">provided</a>' % create_link
|
|
||||||
)))
|
|
||||||
return super(BillAdmin, self).change_view(request, object_id, **kwargs)
|
return super(BillAdmin, self).change_view(request, object_id, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
28
orchestra/apps/bills/helpers.py
Normal file
28
orchestra/apps/bills/helpers.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
def validate_contact(request, bill, error=True):
|
||||||
|
""" checks if all the preconditions for bill generation are met """
|
||||||
|
msg = _('{relation} account "{account}" does not have a declared invoice contact. '
|
||||||
|
'You should <a href="{url}#invoicecontact-group">provide one</a>')
|
||||||
|
valid = True
|
||||||
|
send = messages.error if error else messages.warning
|
||||||
|
if not hasattr(bill.account, 'invoicecontact'):
|
||||||
|
account = force_text(bill.account)
|
||||||
|
url = reverse('admin:accounts_account_change', args=(bill.account_id,))
|
||||||
|
message = msg.format(relation=_("Related"), account=account, url=url)
|
||||||
|
send(request, mark_safe(message))
|
||||||
|
valid = False
|
||||||
|
main = type(bill).account.field.rel.to.get_main()
|
||||||
|
if not hasattr(main, 'invoicecontact'):
|
||||||
|
account = force_text(main)
|
||||||
|
url = reverse('admin:accounts_account_change', args=(main.id,))
|
||||||
|
message = msg.format(relation=_("Main"), account=account, url=url)
|
||||||
|
send(request, mark_safe(message))
|
||||||
|
valid = False
|
||||||
|
return valid
|
||||||
|
|
|
@ -81,7 +81,7 @@ class Bill(models.Model):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def payment_state(self):
|
def payment_state(self):
|
||||||
if self.is_open:
|
if self.is_open or self.get_type() == self.PROFORMA:
|
||||||
return self.OPEN
|
return self.OPEN
|
||||||
secured = self.transactions.secured().amount()
|
secured = self.transactions.secured().amount()
|
||||||
if secured >= self.total:
|
if secured >= self.total:
|
||||||
|
@ -136,12 +136,14 @@ class Bill(models.Model):
|
||||||
self.due_on = self.get_due_date(payment=payment)
|
self.due_on = self.get_due_date(payment=payment)
|
||||||
self.total = self.get_total()
|
self.total = self.get_total()
|
||||||
self.html = self.render(payment=payment)
|
self.html = self.render(payment=payment)
|
||||||
|
transaction = None
|
||||||
if self.get_type() != self.PROFORMA:
|
if self.get_type() != self.PROFORMA:
|
||||||
self.transactions.create(bill=self, source=payment, amount=self.total)
|
transaction = self.transactions.create(bill=self, source=payment, amount=self.total)
|
||||||
self.closed_on = timezone.now()
|
self.closed_on = timezone.now()
|
||||||
self.is_open = False
|
self.is_open = False
|
||||||
self.is_sent = False
|
self.is_sent = False
|
||||||
self.save()
|
self.save()
|
||||||
|
return transaction
|
||||||
|
|
||||||
def send(self):
|
def send(self):
|
||||||
html = self.html or self.render()
|
html = self.html or self.render()
|
||||||
|
|
|
@ -1,2 +1,6 @@
|
||||||
{% if subject %}Bill {{ bill.number }}{% endif %}
|
{% if subject %}Bill {{ bill.number }}{% endif %}
|
||||||
{% if message %}Find attached your invoice{% endif %}
|
{% if message %}Dear {{ bill.account.username }},
|
||||||
|
Find your {{ bill.get_type.lower }} attached.
|
||||||
|
|
||||||
|
If you have any question, please write us at support@orchestra.lan
|
||||||
|
{% endif %}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.utils import plugins
|
from orchestra.utils import plugins
|
||||||
|
|
||||||
|
@ -111,6 +112,11 @@ class ServiceBackend(plugins.Plugin):
|
||||||
|
|
||||||
class ServiceController(ServiceBackend):
|
class ServiceController(ServiceBackend):
|
||||||
actions = ('save', 'delete')
|
actions = ('save', 'delete')
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_verbose_name(cls):
|
||||||
|
return _("[S] %s") % super(ServiceController, cls).get_verbose_name()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_backends(cls):
|
def get_backends(cls):
|
||||||
|
|
|
@ -2,10 +2,11 @@ from django.contrib import admin, messages
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
from django.utils.translation import ungettext
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from orchestra.admin.utils import change_url
|
||||||
|
|
||||||
from .forms import (BillSelectedOptionsForm, BillSelectConfirmationForm,
|
from .forms import (BillSelectedOptionsForm, BillSelectConfirmationForm,
|
||||||
BillSelectRelatedForm)
|
BillSelectRelatedForm)
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ class BillSelectedOrders(object):
|
||||||
self.options = dict(
|
self.options = dict(
|
||||||
billing_point=form.cleaned_data['billing_point'],
|
billing_point=form.cleaned_data['billing_point'],
|
||||||
fixed_point=form.cleaned_data['fixed_point'],
|
fixed_point=form.cleaned_data['fixed_point'],
|
||||||
is_proforma=form.cleaned_data['is_proforma'],
|
proforma=form.cleaned_data['proforma'],
|
||||||
new_open=form.cleaned_data['new_open'],
|
new_open=form.cleaned_data['new_open'],
|
||||||
)
|
)
|
||||||
if int(request.POST.get('step')) != 3:
|
if int(request.POST.get('step')) != 3:
|
||||||
|
@ -78,13 +79,17 @@ class BillSelectedOrders(object):
|
||||||
if int(request.POST.get('step')) >= 3:
|
if int(request.POST.get('step')) >= 3:
|
||||||
bills = self.queryset.bill(commit=True, **self.options)
|
bills = self.queryset.bill(commit=True, **self.options)
|
||||||
for order in self.queryset:
|
for order in self.queryset:
|
||||||
self.modeladmin.log_change(request, order, 'Billed')
|
self.modeladmin.log_change(request, order, _("Billed"))
|
||||||
if not bills:
|
if not bills:
|
||||||
msg = _("Selected orders do not have pending billing")
|
msg = _("Selected orders do not have pending billing")
|
||||||
self.modeladmin.message_user(request, msg, messages.WARNING)
|
self.modeladmin.message_user(request, msg, messages.WARNING)
|
||||||
else:
|
else:
|
||||||
ids = ','.join([str(bill.id) for bill in bills])
|
num = len(bills)
|
||||||
|
if num == 1:
|
||||||
|
url = change_url(bills[0])
|
||||||
|
else:
|
||||||
url = reverse('admin:bills_bill_changelist')
|
url = reverse('admin:bills_bill_changelist')
|
||||||
|
ids = ','.join([str(bill.id) for bill in bills])
|
||||||
url += '?id__in=%s' % ids
|
url += '?id__in=%s' % ids
|
||||||
num = len(bills)
|
num = len(bills)
|
||||||
msg = ungettext(
|
msg = ungettext(
|
||||||
|
|
|
@ -18,7 +18,7 @@ class BillSelectedOptionsForm(AdminFormMixin, forms.Form):
|
||||||
label=_("Fixed point"),
|
label=_("Fixed point"),
|
||||||
help_text=_("Deisgnates whether you want the billing point to be an "
|
help_text=_("Deisgnates whether you want the billing point to be an "
|
||||||
"exact date, or adapt it to the billing period."))
|
"exact date, or adapt it to the billing period."))
|
||||||
is_proforma = forms.BooleanField(initial=False, required=False,
|
proforma = forms.BooleanField(initial=False, required=False,
|
||||||
label=_("Pro-forma (billing simulation)"),
|
label=_("Pro-forma (billing simulation)"),
|
||||||
help_text=_("Creates a Pro Forma instead of billing the orders."))
|
help_text=_("Creates a Pro Forma instead of billing the orders."))
|
||||||
new_open = forms.BooleanField(initial=False, required=False,
|
new_open = forms.BooleanField(initial=False, required=False,
|
||||||
|
@ -49,7 +49,7 @@ class BillSelectRelatedForm(AdminFormMixin, forms.Form):
|
||||||
required=False)
|
required=False)
|
||||||
billing_point = forms.DateField(widget=forms.HiddenInput())
|
billing_point = forms.DateField(widget=forms.HiddenInput())
|
||||||
fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
is_proforma = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
proforma = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -63,5 +63,5 @@ class BillSelectRelatedForm(AdminFormMixin, forms.Form):
|
||||||
class BillSelectConfirmationForm(AdminFormMixin, forms.Form):
|
class BillSelectConfirmationForm(AdminFormMixin, forms.Form):
|
||||||
billing_point = forms.DateField(widget=forms.HiddenInput())
|
billing_point = forms.DateField(widget=forms.HiddenInput())
|
||||||
fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
is_proforma = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
proforma = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False)
|
||||||
|
|
|
@ -92,7 +92,7 @@ class TransactionAdmin(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdm
|
||||||
|
|
||||||
class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
||||||
list_display = ('id', 'file_url', 'display_transactions', 'created_at')
|
list_display = ('id', 'file_url', 'display_transactions', 'created_at')
|
||||||
fields = ('data', 'file_url', 'display_transactions', 'created_at')
|
fields = ('data', 'file_url', 'created_at')
|
||||||
readonly_fields = ('file_url', 'display_transactions', 'created_at')
|
readonly_fields = ('file_url', 'display_transactions', 'created_at')
|
||||||
inlines = [TransactionInline]
|
inlines = [TransactionInline]
|
||||||
actions = (actions.mark_process_as_executed, actions.abort, actions.commit)
|
actions = (actions.mark_process_as_executed, actions.abort, actions.commit)
|
||||||
|
@ -111,16 +111,17 @@ class TransactionProcessAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
|
||||||
# Because of values_list this query doesn't benefit from prefetch_related
|
# Because of values_list this query doesn't benefit from prefetch_related
|
||||||
tx_ids = process.transactions.values_list('id', flat=True)
|
tx_ids = process.transactions.values_list('id', flat=True)
|
||||||
for tx_id in tx_ids:
|
for tx_id in tx_ids:
|
||||||
ids.append(str(tx_id))
|
ids.append('#%i' % tx_id)
|
||||||
counter += 1
|
counter += 1
|
||||||
if counter > 10:
|
if counter > 10:
|
||||||
counter = 0
|
counter = 0
|
||||||
lines.append(','.join(ids))
|
lines.append(','.join(ids))
|
||||||
ids = []
|
ids = []
|
||||||
lines.append(','.join(ids))
|
lines.append(','.join(ids))
|
||||||
|
transactions = '<br'.join(lines)
|
||||||
url = reverse('admin:payments_transaction_changelist')
|
url = reverse('admin:payments_transaction_changelist')
|
||||||
url += '?processes=%i' % process.id
|
url += '?processes=%i' % process.id
|
||||||
return '<a href="%s">%s</a>' % (url, '<br>'.join(lines))
|
return '<a href="%s">%s</a>' % (url, transactions)
|
||||||
display_transactions.short_description = _("Transactions")
|
display_transactions.short_description = _("Transactions")
|
||||||
display_transactions.allow_tags = True
|
display_transactions.allow_tags = True
|
||||||
|
|
||||||
|
|
|
@ -175,10 +175,10 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
def get_debt_transactions(cls, transactions, process):
|
def get_debt_transactions(cls, transactions, process):
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
transaction.process = process
|
transaction.process = process
|
||||||
|
transaction.state = transaction.WAITTING_EXECUTION
|
||||||
|
transaction.save(update_fields=['state', 'process'])
|
||||||
account = transaction.account
|
account = transaction.account
|
||||||
data = transaction.source.data
|
data = transaction.source.data
|
||||||
transaction.state = transaction.WAITTING_CONFIRMATION
|
|
||||||
transaction.save(update_fields=['state'])
|
|
||||||
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
|
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
|
||||||
E.PmtId( # Payment Id
|
E.PmtId( # Payment Id
|
||||||
E.EndToEndId(str(transaction.id)) # Payment Id/End to End
|
E.EndToEndId(str(transaction.id)) # Payment Id/End to End
|
||||||
|
@ -191,7 +191,7 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
E.MndtRltdInf( # Mandate Related Info
|
E.MndtRltdInf( # Mandate Related Info
|
||||||
E.MndtId(str(account.id)), # Mandate Id
|
E.MndtId(str(account.id)), # Mandate Id
|
||||||
E.DtOfSgntr( # Date of Signature
|
E.DtOfSgntr( # Date of Signature
|
||||||
account.register_date.strftime("%Y-%m-%d")
|
account.date_joined.strftime("%Y-%m-%d")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -216,10 +216,10 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
def get_credit_transactions(transactions, process):
|
def get_credit_transactions(transactions, process):
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
transaction.process = process
|
transaction.process = process
|
||||||
|
transaction.state = transaction.WAITTING_EXECUTION
|
||||||
|
transaction.save(update_fields=['state', 'process'])
|
||||||
account = transaction.account
|
account = transaction.account
|
||||||
data = transaction.source.data
|
data = transaction.source.data
|
||||||
transaction.state = transaction.WAITTING_CONFIRMATION
|
|
||||||
transaction.save(update_fields=['state'])
|
|
||||||
yield E.CdtTrfTxInf( # Credit Transfer Transaction Info
|
yield E.CdtTrfTxInf( # Credit Transfer Transaction Info
|
||||||
E.PmtId( # Payment Id
|
E.PmtId( # Payment Id
|
||||||
E.EndToEndId(str(transaction.id)) # Payment Id/End to End
|
E.EndToEndId(str(transaction.id)) # Payment Id/End to End
|
||||||
|
|
|
@ -104,7 +104,7 @@ class Transaction(models.Model):
|
||||||
objects = TransactionQuerySet.as_manager()
|
objects = TransactionQuerySet.as_manager()
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return "Transaction {}".format(self.id)
|
return "Transaction #{}".format(self.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account(self):
|
def account(self):
|
||||||
|
@ -162,7 +162,7 @@ class TransactionProcess(models.Model):
|
||||||
verbose_name_plural = _("Transaction processes")
|
verbose_name_plural = _("Transaction processes")
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return str(self.id)
|
return '#%i' % self.id
|
||||||
|
|
||||||
def mark_as_executed(self):
|
def mark_as_executed(self):
|
||||||
assert self.state == self.CREATED
|
assert self.state == self.CREATED
|
||||||
|
|
|
@ -3,6 +3,7 @@ import datetime
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.apps.orchestration import ServiceBackend
|
from orchestra.apps.orchestration import ServiceBackend
|
||||||
|
|
||||||
|
@ -13,15 +14,19 @@ class ServiceMonitor(ServiceBackend):
|
||||||
MEMORY = 'memory'
|
MEMORY = 'memory'
|
||||||
CPU = 'cpu'
|
CPU = 'cpu'
|
||||||
# TODO UNITS
|
# TODO UNITS
|
||||||
|
|
||||||
actions = ('monitor', 'exceeded', 'recovery')
|
actions = ('monitor', 'exceeded', 'recovery')
|
||||||
|
abstract = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_backends(cls):
|
def get_backends(cls):
|
||||||
""" filter monitor classes """
|
""" filter controller classes """
|
||||||
for backend in cls.plugins:
|
return [
|
||||||
if backend != ServiceMonitor and ServiceMonitor in backend.__mro__:
|
plugin for plugin in cls.plugins if ServiceMonitor in plugin.__mro__
|
||||||
yield backend
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_verbose_name(cls):
|
||||||
|
return _("[M] %s") % super(ServiceMonitor, cls).get_verbose_name()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def current_date(self):
|
def current_date(self):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.forms import PluginDataForm
|
from orchestra.forms import PluginDataForm
|
||||||
|
@ -6,16 +7,24 @@ from orchestra.forms import PluginDataForm
|
||||||
from .options import SoftwareService
|
from .options import SoftwareService
|
||||||
|
|
||||||
|
|
||||||
class WordpressForm(PluginDataForm):
|
class WordPressForm(PluginDataForm):
|
||||||
username = forms.CharField(label=_("Username"), max_length=64)
|
username = forms.CharField(label=_("Username"), max_length=64)
|
||||||
password = forms.CharField(label=_("Password"), max_length=64)
|
password = forms.CharField(label=_("Password"), max_length=64)
|
||||||
site_name = forms.CharField(label=_("Site name"), max_length=64,
|
site_name = forms.CharField(label=_("Site name"), max_length=64,
|
||||||
help_text=_("URL will be <site_name>.blogs.orchestra.lan"))
|
help_text=_("URL will be <site_name>.blogs.orchestra.lan"))
|
||||||
email = forms.EmailField(label=_("Email"))
|
email = forms.EmailField(label=_("Email"))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(WordPressForm, self).__init__(*args, **kwargs)
|
||||||
|
instance = kwargs.get('instance')
|
||||||
|
if instance:
|
||||||
|
url = 'http://%s.%s' % (instance.data['site_name'], 'blogs.orchestra.lan')
|
||||||
|
url = '<a href="%s">%s</a>' % (url, url)
|
||||||
|
self.fields['site_name'].help_text = mark_safe(url)
|
||||||
|
|
||||||
|
|
||||||
class WordpressService(SoftwareService):
|
class WordpressService(SoftwareService):
|
||||||
verbose_name = "WordPress"
|
verbose_name = "WordPress"
|
||||||
form = WordpressForm
|
form = WordPressForm
|
||||||
description_field = 'site_name'
|
description_field = 'site_name'
|
||||||
icon = 'saas/icons/WordPress.png'
|
icon = 'saas/icons/WordPress.png'
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for plugin in plugins %}
|
{% for plugin in plugins %}
|
||||||
<li><a style="font-size:small;" href="../?{{ field }}={{ plugin.get_name }}&{{ request.META.QUERY_STRING }}">{{ plugin.verbose_name }}</<a></li>
|
<li><a style="font-size:small;" href="../?{{ field }}={{ plugin.get_plugin_name }}&{{ request.META.QUERY_STRING }}">{{ plugin.verbose_name }}</<a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -24,22 +24,26 @@ class Plugin(object):
|
||||||
raise KeyError('This plugin is not registered')
|
raise KeyError('This plugin is not registered')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_plugin_choices(cls):
|
def get_verbose_name(cls):
|
||||||
plugins = cls.get_plugins()
|
|
||||||
choices = []
|
|
||||||
for p in plugins:
|
|
||||||
# don't evaluate p.verbose_name ugettext_lazy
|
# don't evaluate p.verbose_name ugettext_lazy
|
||||||
verbose = getattr(p.verbose_name, '_proxy____args', [p.verbose_name])
|
verbose = getattr(cls.verbose_name, '_proxy____args', [cls.verbose_name])
|
||||||
if verbose[0]:
|
if verbose[0]:
|
||||||
verbose = p.verbose_name
|
return cls.verbose_name
|
||||||
else:
|
else:
|
||||||
verbose = p.get_plugin_name()
|
return cls.get_plugin_name()
|
||||||
choices.append((p.get_plugin_name(), verbose))
|
|
||||||
return sorted(choices, key=lambda e: e[0])
|
@classmethod
|
||||||
|
def get_plugin_choices(cls):
|
||||||
|
choices = []
|
||||||
|
for plugin in cls.get_plugins():
|
||||||
|
verbose = plugin.get_verbose_name()
|
||||||
|
choices.append((plugin.get_plugin_name(), verbose))
|
||||||
|
return sorted(choices, key=lambda e: e[1])
|
||||||
|
|
||||||
|
|
||||||
class PluginMount(type):
|
class PluginMount(type):
|
||||||
def __init__(cls, name, bases, attrs):
|
def __init__(cls, name, bases, attrs):
|
||||||
|
if not attrs.get('abstract', False):
|
||||||
if not hasattr(cls, 'plugins'):
|
if not hasattr(cls, 'plugins'):
|
||||||
# This branch only executes when processing the mount point itself.
|
# This branch only executes when processing the mount point itself.
|
||||||
# So, since this is a new plugin type, not an implementation, this
|
# So, since this is a new plugin type, not an implementation, this
|
||||||
|
|
48
scripts/services/mailman.md
Normal file
48
scripts/services/mailman.md
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
apt-get install mailman
|
||||||
|
|
||||||
|
newlist mailman
|
||||||
|
|
||||||
|
|
||||||
|
echo 'mailman: "|/var/lib/mailman/mail/mailman post mailman"
|
||||||
|
mailman-admin: "|/var/lib/mailman/mail/mailman admin mailman"
|
||||||
|
mailman-bounces: "|/var/lib/mailman/mail/mailman bounces mailman"
|
||||||
|
mailman-confirm: "|/var/lib/mailman/mail/mailman confirm mailman"
|
||||||
|
mailman-join: "|/var/lib/mailman/mail/mailman join mailman"
|
||||||
|
mailman-leave: "|/var/lib/mailman/mail/mailman leave mailman"
|
||||||
|
mailman-owner: "|/var/lib/mailman/mail/mailman owner mailman"
|
||||||
|
mailman-request: "|/var/lib/mailman/mail/mailman request mailman"
|
||||||
|
mailman-subscribe: "|/var/lib/mailman/mail/mailman subscribe mailman"
|
||||||
|
mailman-unsubscribe: "|/var/lib/mailman/mail/mailman unsubscribe mailman"' >> /etc/aliases
|
||||||
|
|
||||||
|
|
||||||
|
postalias /etc/aliases
|
||||||
|
|
||||||
|
|
||||||
|
/etc/init.d/mailman start
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# postifx
|
||||||
|
|
||||||
|
relay_domains = $mydestination, lists.orchestra.lan
|
||||||
|
relay_recipient_maps = hash:/var/lib/mailman/data/aliases
|
||||||
|
transport_maps = hash:/etc/postfix/transport
|
||||||
|
mailman_destination_recipient_limit = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo "lists.orchestra.lan mailman:" >> /etc/postfix/transport
|
||||||
|
|
||||||
|
postmap /etc/postfix/transport
|
||||||
|
|
||||||
|
echo 'MTA = "Postfix"
|
||||||
|
POSTFIX_STYLE_VIRTUAL_DOMAINS = ["lists.orchestra.lan"]
|
||||||
|
# alias for postmaster, abuse and mailer-daemon
|
||||||
|
DEB_LISTMASTER = "postmaster@orchestra.lan"' >> /etc/mailman/mm_cfg.py
|
||||||
|
|
||||||
|
|
||||||
|
sed -i "s/DEFAULT_EMAIL_HOST\s*=\s*.*/DEFAULT_EMAIL_HOST = 'lists.orchestra.lan'/" /etc/mailman/mm_cfg.py
|
||||||
|
sed -i "s/DEFAULT_URL_HOST\s*=\s*.*/DEFAULT_URL_HOST = 'lists.orchestra.lan'/" /etc/mailman/mm_cfg.py
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue