Fixes on billing
This commit is contained in:
parent
9ac9f6b933
commit
28f644f4e6
7
TODO.md
7
TODO.md
|
@ -282,8 +282,7 @@ https://code.djangoproject.com/ticket/24576
|
||||||
# bill.totals make it 100% computed?
|
# bill.totals make it 100% computed?
|
||||||
* joomla: wget https://github.com/joomla/joomla-cms/releases/download/3.4.1/Joomla_3.4.1-Stable-Full_Package.tar.gz -O - | tar xvfz -
|
* joomla: wget https://github.com/joomla/joomla-cms/releases/download/3.4.1/Joomla_3.4.1-Stable-Full_Package.tar.gz -O - | tar xvfz -
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Link related orders on bill line
|
|
||||||
# Customize those service.descriptions that are
|
|
||||||
# replace multichoicefield and jsonfield by ArrayField, HStoreField
|
# replace multichoicefield and jsonfield by ArrayField, HStoreField
|
||||||
|
# Amend lines???
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,12 @@ from django.contrib.admin.options import IS_POPUP_VAR
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect, Http404
|
||||||
from django.forms.models import BaseInlineFormSet
|
from django.forms.models import BaseInlineFormSet
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.encoding import force_text
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.decorators.debug import sensitive_post_parameters
|
from django.views.decorators.debug import sensitive_post_parameters
|
||||||
|
@ -165,6 +166,14 @@ class ExtendedModelAdmin(ChangeViewActionsMixin, ChangeAddFieldsMixin, admin.Mod
|
||||||
qs = qs.prefetch_related(*self.list_prefetch_related)
|
qs = qs.prefetch_related(*self.list_prefetch_related)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
def get_object(self, request, object_id, from_field=None):
|
||||||
|
obj = super(ExtendedModelAdmin, self).get_object(request, object_id, from_field)
|
||||||
|
if obj is None:
|
||||||
|
opts = self.model._meta
|
||||||
|
raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {
|
||||||
|
'name': force_text(opts.verbose_name), 'key': escape(object_id)})
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class ChangePasswordAdminMixin(object):
|
class ChangePasswordAdminMixin(object):
|
||||||
change_password_form = AdminPasswordChangeForm
|
change_password_form = AdminPasswordChangeForm
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render, redirect
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ungettext, ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ def close_bills(modeladmin, request, queryset):
|
||||||
url = change_url(transactions[0])
|
url = change_url(transactions[0])
|
||||||
else:
|
else:
|
||||||
url = reverse('admin:transactions_transaction_changelist')
|
url = reverse('admin:transactions_transaction_changelist')
|
||||||
url += 'id__in=%s' % ','.join([str(t.id) for t in transactions])
|
url += 'id__in=%s' % ','.join(map(str, transactions))
|
||||||
message = ungettext(
|
message = ungettext(
|
||||||
_('<a href="%s">One related transaction</a> has been created') % url,
|
_('<a href="%s">One related transaction</a> has been created') % url,
|
||||||
_('<a href="%s">%i related transactions</a> have been created') % (url, num),
|
_('<a href="%s">%i related transactions</a> have been created') % (url, num),
|
||||||
|
@ -114,6 +114,12 @@ send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', Fa
|
||||||
send_bills.url_name = 'send'
|
send_bills.url_name = 'send'
|
||||||
|
|
||||||
|
|
||||||
|
def manage_lines(modeladmin, request, queryset):
|
||||||
|
url = reverse('admin:bills_bill_manage_lines')
|
||||||
|
url += '?ids=%s' % ','.join(map(str, queryset.values_list('id', flat=True)))
|
||||||
|
return redirect(url)
|
||||||
|
|
||||||
|
|
||||||
def undo_billing(modeladmin, request, queryset):
|
def undo_billing(modeladmin, request, queryset):
|
||||||
group = {}
|
group = {}
|
||||||
for line in queryset.select_related('order'):
|
for line in queryset.select_related('order'):
|
||||||
|
@ -125,7 +131,7 @@ def undo_billing(modeladmin, request, queryset):
|
||||||
# TODO force incomplete info
|
# TODO force incomplete info
|
||||||
for order, lines in group.items():
|
for order, lines in group.items():
|
||||||
# Find path from ini to end
|
# Find path from ini to end
|
||||||
for attr in ['order_id', 'order_billed_on', 'order_billed_until']:
|
for attr in ('order_id', 'order_billed_on', 'order_billed_until'):
|
||||||
if not getattr(self, attr):
|
if not getattr(self, attr):
|
||||||
raise ValidationError(_("Not enough information stored for undoing"))
|
raise ValidationError(_("Not enough information stored for undoing"))
|
||||||
sorted(lines, key=lambda l: l.created_on)
|
sorted(lines, key=lambda l: l.created_on)
|
||||||
|
@ -144,8 +150,8 @@ def move_lines(modeladmin, request, queryset):
|
||||||
account = None
|
account = None
|
||||||
for line in queryset.select_related('bill'):
|
for line in queryset.select_related('bill'):
|
||||||
bill = line.bill
|
bill = line.bill
|
||||||
if bill.state != bill.OPEN:
|
if not bill.is_open:
|
||||||
messages.error(request, _("Can not move lines which are not in open state."))
|
messages.error(request, _("Can not move lines from a closed bill."))
|
||||||
return
|
return
|
||||||
elif not account:
|
elif not account:
|
||||||
account = bill.account
|
account = bill.account
|
||||||
|
@ -155,6 +161,7 @@ def move_lines(modeladmin, request, queryset):
|
||||||
target = request.GET.get('target')
|
target = request.GET.get('target')
|
||||||
if not target:
|
if not target:
|
||||||
# select target
|
# select target
|
||||||
|
context = {}
|
||||||
return render(request, 'admin/orchestra/generic_confirmation.html', context)
|
return render(request, 'admin/orchestra/generic_confirmation.html', context)
|
||||||
target = Bill.objects.get(pk=int(pk))
|
target = Bill.objects.get(pk=int(pk))
|
||||||
if target.account != account:
|
if target.account != account:
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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
|
||||||
from django.db.models import F, Sum
|
from django.db.models import F, Sum, Prefetch
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -17,7 +17,7 @@ from orchestra.forms.widgets import paddingCheckboxSelectMultiple
|
||||||
|
|
||||||
from . import settings, actions
|
from . import settings, actions
|
||||||
from .filters import BillTypeListFilter, HasBillContactListFilter
|
from .filters import BillTypeListFilter, HasBillContactListFilter
|
||||||
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillContact
|
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillSubline, BillContact
|
||||||
|
|
||||||
|
|
||||||
PAYMENT_STATE_COLORS = {
|
PAYMENT_STATE_COLORS = {
|
||||||
|
@ -58,7 +58,7 @@ class BillLineInline(admin.TabularInline):
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(BillLineInline, self).get_queryset(request)
|
qs = super(BillLineInline, self).get_queryset(request)
|
||||||
return qs.prefetch_related('sublines')
|
return qs.prefetch_related('sublines').select_related('order')
|
||||||
|
|
||||||
|
|
||||||
class ClosedBillLineInline(BillLineInline):
|
class ClosedBillLineInline(BillLineInline):
|
||||||
|
@ -99,29 +99,35 @@ class ClosedBillLineInline(BillLineInline):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class BillLineManagerAdmin(admin.ModelAdmin):
|
class BillLineAdmin(admin.ModelAdmin):
|
||||||
list_display = ('description', 'rate', 'quantity', 'tax', 'subtotal')
|
list_display = ('description', 'bill_link', 'rate', 'quantity', 'tax', 'subtotal')
|
||||||
actions = (actions.undo_billing, actions.move_lines, actions.copy_lines,)
|
actions = (actions.undo_billing, actions.move_lines, actions.copy_lines,)
|
||||||
|
list_select_related = ('bill',)
|
||||||
|
|
||||||
|
bill_link = admin_link('bill')
|
||||||
|
|
||||||
|
|
||||||
|
class BillLineManagerAdmin(BillLineAdmin):
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qset = super(BillLineManagerAdmin, self).get_queryset(request)
|
qset = super(BillLineManagerAdmin, self).get_queryset(request)
|
||||||
|
if self.bill_ids:
|
||||||
return qset.filter(bill_id__in=self.bill_ids)
|
return qset.filter(bill_id__in=self.bill_ids)
|
||||||
|
return qset
|
||||||
|
|
||||||
def changelist_view(self, request, extra_context=None):
|
def changelist_view(self, request, extra_context=None):
|
||||||
GET = request.GET.copy()
|
GET = request.GET.copy()
|
||||||
bill_ids = GET.pop('bill_ids', ['0'])[0]
|
bill_ids = GET.pop('ids', None)
|
||||||
|
if bill_ids:
|
||||||
request.GET = GET
|
request.GET = GET
|
||||||
bill_ids = [int(id) for id in bill_ids.split(',')]
|
bill_ids = list(map(int, bill_ids.split(',')))
|
||||||
self.bill_ids = bill_ids
|
self.bill_ids = bill_ids
|
||||||
if not bill_ids:
|
if bill_ids and len(bill_ids) == 1:
|
||||||
return
|
|
||||||
elif len(bill_ids) > 1:
|
|
||||||
title = _("Manage bill lines of multiple bills.")
|
|
||||||
else:
|
|
||||||
bill_url = reverse('admin:bills_bill_change', args=(bill_ids[0],))
|
bill_url = reverse('admin:bills_bill_change', args=(bill_ids[0],))
|
||||||
bill = Bill.objects.get(pk=bill_ids[0])
|
bill = Bill.objects.get(pk=bill_ids[0])
|
||||||
bill_link = '<a href="%s">%s</a>' % (bill_url, bill.ident)
|
bill_link = '<a href="%s">%s</a>' % (bill_url, bill.number)
|
||||||
title = mark_safe(_("Manage %s bill lines.") % bill_link)
|
title = mark_safe(_("Manage %s bill lines.") % bill_link)
|
||||||
|
else:
|
||||||
|
title = _("Manage bill lines of multiple bills.")
|
||||||
context = {
|
context = {
|
||||||
'title': title,
|
'title': title,
|
||||||
}
|
}
|
||||||
|
@ -147,8 +153,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
change_view_actions = [
|
change_view_actions = [
|
||||||
actions.view_bill, actions.download_bills, actions.send_bills, actions.close_bills
|
actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills,
|
||||||
|
actions.close_bills
|
||||||
]
|
]
|
||||||
|
list_prefetch_related = ('transactions',)
|
||||||
search_fields = ('number', 'account__username', 'comments')
|
search_fields = ('number', 'account__username', 'comments')
|
||||||
actions = [actions.download_bills, actions.close_bills, actions.send_bills]
|
actions = [actions.download_bills, actions.close_bills, actions.send_bills]
|
||||||
change_readonly_fields = ('account_link', 'type', 'is_open')
|
change_readonly_fields = ('account_link', 'type', 'is_open')
|
||||||
|
@ -245,7 +253,6 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
(F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100)
|
(F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
qs = qs.prefetch_related('transactions')
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def change_view(self, request, object_id, **kwargs):
|
def change_view(self, request, object_id, **kwargs):
|
||||||
|
@ -261,6 +268,7 @@ admin.site.register(AmendmentInvoice, BillAdmin)
|
||||||
admin.site.register(Fee, BillAdmin)
|
admin.site.register(Fee, BillAdmin)
|
||||||
admin.site.register(AmendmentFee, BillAdmin)
|
admin.site.register(AmendmentFee, BillAdmin)
|
||||||
admin.site.register(ProForma, BillAdmin)
|
admin.site.register(ProForma, BillAdmin)
|
||||||
|
admin.site.register(BillLine, BillLineAdmin)
|
||||||
|
|
||||||
|
|
||||||
class BillContactInline(admin.StackedInline):
|
class BillContactInline(admin.StackedInline):
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -290,17 +290,12 @@ class BillLine(models.Model):
|
||||||
related_name='amendment_lines', null=True, blank=True)
|
related_name='amendment_lines', null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "#%i" % self.number
|
return "#%i" % self.pk
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def number(self):
|
|
||||||
lines = type(self).objects.filter(bill=self.bill_id)
|
|
||||||
return lines.filter(id__lte=self.id).order_by('id').count()
|
|
||||||
|
|
||||||
def get_total(self):
|
def get_total(self):
|
||||||
""" Computes subline discounts """
|
""" Computes subline discounts """
|
||||||
if self.pk:
|
if self.pk:
|
||||||
return self.subtotal + sum(self.sublines.values_list('total', flat=True))
|
return self.subtotal + sum([sub.total for sub in self.sublines.all()])
|
||||||
|
|
||||||
def get_verbose_quantity(self):
|
def get_verbose_quantity(self):
|
||||||
return self.verbose_quantity or self.quantity
|
return self.verbose_quantity or self.quantity
|
||||||
|
|
|
@ -17,11 +17,11 @@ class MySQLBackend(ServiceController):
|
||||||
if database.type != database.MYSQL:
|
if database.type != database.MYSQL:
|
||||||
return
|
return
|
||||||
context = self.get_context(database)
|
context = self.get_context(database)
|
||||||
# Not available on delete()
|
|
||||||
context['owner'] = database.owner
|
|
||||||
self.append(
|
self.append(
|
||||||
"mysql -e 'CREATE DATABASE `%(database)s`;' || true" % context
|
"mysql -e 'CREATE DATABASE `%(database)s`;' || true" % context
|
||||||
)
|
)
|
||||||
|
# Not available on delete()
|
||||||
|
context['owner'] = database.owner
|
||||||
# clean previous privileges
|
# clean previous privileges
|
||||||
self.append("""mysql mysql -e 'DELETE FROM db WHERE db = "%(database)s";'""" % context)
|
self.append("""mysql mysql -e 'DELETE FROM db WHERE db = "%(database)s";'""" % context)
|
||||||
for user in database.users.all():
|
for user in database.users.all():
|
||||||
|
|
|
@ -15,7 +15,7 @@ class Database(models.Model):
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=64, # MySQL limit
|
name = models.CharField(_("name"), max_length=64, # MySQL limit
|
||||||
validators=[validators.validate_name])
|
validators=[validators.validate_name])
|
||||||
users = models.ManyToManyField('databases.DatabaseUser',
|
users = models.ManyToManyField('databases.DatabaseUser', blank=True,
|
||||||
verbose_name=_("users"),related_name='databases')
|
verbose_name=_("users"),related_name='databases')
|
||||||
type = models.CharField(_("type"), max_length=32,
|
type = models.CharField(_("type"), max_length=32,
|
||||||
choices=settings.DATABASES_TYPE_CHOICES,
|
choices=settings.DATABASES_TYPE_CHOICES,
|
||||||
|
@ -34,7 +34,10 @@ class Database(models.Model):
|
||||||
""" database owner is the first user related to it """
|
""" database owner is the first user related to it """
|
||||||
# Accessing intermediary model to get which is the first user
|
# Accessing intermediary model to get which is the first user
|
||||||
users = Database.users.through.objects.filter(database_id=self.id)
|
users = Database.users.through.objects.filter(database_id=self.id)
|
||||||
return users.order_by('id').first().databaseuser
|
user = users.order_by('id').first()
|
||||||
|
if user is not None:
|
||||||
|
return user.databaseuser
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
Database.users.through._meta.unique_together = (
|
Database.users.through._meta.unique_together = (
|
||||||
|
|
|
@ -89,7 +89,7 @@ class BillSelectedOrders(object):
|
||||||
url = change_url(bills[0])
|
url = change_url(bills[0])
|
||||||
else:
|
else:
|
||||||
url = reverse('admin:bills_bill_changelist')
|
url = reverse('admin:bills_bill_changelist')
|
||||||
ids = ','.join([str(bill.id) for bill in bills])
|
ids = ','.join(map(str, bills))
|
||||||
url += '?id__in=%s' % ids
|
url += '?id__in=%s' % ids
|
||||||
num = len(bills)
|
num = len(bills)
|
||||||
msg = ungettext(
|
msg = ungettext(
|
||||||
|
|
|
@ -10,7 +10,7 @@ from .models import Plan, ContractedPlan, Rate
|
||||||
|
|
||||||
class RateInline(admin.TabularInline):
|
class RateInline(admin.TabularInline):
|
||||||
model = Rate
|
model = Rate
|
||||||
ordering = ('plan', 'quantity')
|
ordering = ('service', 'plan', 'quantity')
|
||||||
|
|
||||||
|
|
||||||
class PlanAdmin(ExtendedModelAdmin):
|
class PlanAdmin(ExtendedModelAdmin):
|
||||||
|
|
|
@ -12,7 +12,7 @@ def run_monitor(modeladmin, request, queryset):
|
||||||
for resource in queryset:
|
for resource in queryset:
|
||||||
rlogs = resource.monitor()
|
rlogs = resource.monitor()
|
||||||
if not async:
|
if not async:
|
||||||
logs = logs.union(set([str(rlog.pk) for rlog in rlogs]))
|
logs = logs.union(set(map(str, rlogs)))
|
||||||
modeladmin.log_change(request, resource, _("Run monitors"))
|
modeladmin.log_change(request, resource, _("Run monitors"))
|
||||||
if async:
|
if async:
|
||||||
num = len(queryset)
|
num = len(queryset)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from orchestra.contrib.orchestration import ServiceController
|
||||||
from . import WebAppServiceMixin
|
from . import WebAppServiceMixin
|
||||||
|
|
||||||
|
|
||||||
|
# TODO DEPRECATE
|
||||||
class WebalizerAppBackend(WebAppServiceMixin, ServiceController):
|
class WebalizerAppBackend(WebAppServiceMixin, ServiceController):
|
||||||
""" Needed for cleaning up webalizer main folder when webapp deleteion withou related contents """
|
""" Needed for cleaning up webalizer main folder when webapp deleteion withou related contents """
|
||||||
verbose_name = _("Webalizer App")
|
verbose_name = _("Webalizer App")
|
||||||
|
|
|
@ -28,10 +28,11 @@ class WebalizerBackend(ServiceController):
|
||||||
|
|
||||||
def delete(self, content):
|
def delete(self, content):
|
||||||
context = self.get_context(content)
|
context = self.get_context(content)
|
||||||
delete_webapp = type(content.webapp).objects.filter(pk=content.webapp.pk).exists()
|
delete_webapp = not type(content.webapp).objects.filter(pk=content.webapp.pk).exists()
|
||||||
if delete_webapp:
|
if delete_webapp:
|
||||||
self.append("rm -f %(webapp_path)s" % context)
|
self.append("rm -fr %(webapp_path)s" % context)
|
||||||
if delete_webapp or not content.webapp.content_set.filter(website=content.website).exists():
|
remounted = content.webapp.content_set.filter(website=content.website).exists()
|
||||||
|
if delete_webapp or not remounted:
|
||||||
self.append("rm -fr %(webalizer_path)s" % context)
|
self.append("rm -fr %(webalizer_path)s" % context)
|
||||||
self.append("rm -f %(webalizer_conf_path)s" % context)
|
self.append("rm -f %(webalizer_conf_path)s" % context)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ def html_to_pdf(html):
|
||||||
return run(
|
return run(
|
||||||
'PATH=$PATH:/usr/local/bin/\n'
|
'PATH=$PATH:/usr/local/bin/\n'
|
||||||
'xvfb-run -a -s "-screen 0 640x4800x16" '
|
'xvfb-run -a -s "-screen 0 640x4800x16" '
|
||||||
'wkhtmltopdf -q --footer-center "Page [page] of [topage]" --footer-font-size 9 - -',
|
'wkhtmltopdf -q --footer-center "Page [page] of [topage]" '
|
||||||
|
' --footer-font-size 9 --margin-bottom 20 --margin-top 20 - -',
|
||||||
stdin=html.encode('utf-8')
|
stdin=html.encode('utf-8')
|
||||||
).stdout
|
).stdout
|
||||||
|
|
Loading…
Reference in a new issue