Fixes on billing

This commit is contained in:
Marc Aymerich 2015-04-21 13:12:48 +00:00
parent 9ac9f6b933
commit 28f644f4e6
14 changed files with 98 additions and 45 deletions

View file

@ -282,8 +282,7 @@ https://code.djangoproject.com/ticket/24576
# 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 -
# Link related orders on bill line
# Customize those service.descriptions that are
# replace multichoicefield and jsonfield by ArrayField, HStoreField
# Amend lines???

View file

@ -5,11 +5,12 @@ from django.contrib.admin.options import IS_POPUP_VAR
from django.contrib.admin.utils import unquote
from django.contrib.auth import update_session_auth_hash
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.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.utils.encoding import force_text
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.debug import sensitive_post_parameters
@ -164,6 +165,14 @@ class ExtendedModelAdmin(ChangeViewActionsMixin, ChangeAddFieldsMixin, admin.Mod
if self.list_prefetch_related:
qs = qs.prefetch_related(*self.list_prefetch_related)
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):

View file

@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import transaction
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.translation import ungettext, ugettext_lazy as _
@ -76,7 +76,7 @@ def close_bills(modeladmin, request, queryset):
url = change_url(transactions[0])
else:
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(
_('<a href="%s">One related transaction</a> has been created') % url,
_('<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'
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):
group = {}
for line in queryset.select_related('order'):
@ -125,7 +131,7 @@ def undo_billing(modeladmin, request, queryset):
# TODO force incomplete info
for order, lines in group.items():
# 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):
raise ValidationError(_("Not enough information stored for undoing"))
sorted(lines, key=lambda l: l.created_on)
@ -144,8 +150,8 @@ def move_lines(modeladmin, request, queryset):
account = None
for line in queryset.select_related('bill'):
bill = line.bill
if bill.state != bill.OPEN:
messages.error(request, _("Can not move lines which are not in open state."))
if not bill.is_open:
messages.error(request, _("Can not move lines from a closed bill."))
return
elif not account:
account = bill.account
@ -155,6 +161,7 @@ def move_lines(modeladmin, request, queryset):
target = request.GET.get('target')
if not target:
# select target
context = {}
return render(request, 'admin/orchestra/generic_confirmation.html', context)
target = Bill.objects.get(pk=int(pk))
if target.account != account:

View file

@ -4,7 +4,7 @@ from django.contrib import admin
from django.contrib.admin.utils import unquote
from django.core.urlresolvers import reverse
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.templatetags.static import static
from django.utils.safestring import mark_safe
@ -17,7 +17,7 @@ from orchestra.forms.widgets import paddingCheckboxSelectMultiple
from . import settings, actions
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 = {
@ -58,7 +58,7 @@ class BillLineInline(admin.TabularInline):
def get_queryset(self, request):
qs = super(BillLineInline, self).get_queryset(request)
return qs.prefetch_related('sublines')
return qs.prefetch_related('sublines').select_related('order')
class ClosedBillLineInline(BillLineInline):
@ -99,29 +99,35 @@ class ClosedBillLineInline(BillLineInline):
return False
class BillLineManagerAdmin(admin.ModelAdmin):
list_display = ('description', 'rate', 'quantity', 'tax', 'subtotal')
class BillLineAdmin(admin.ModelAdmin):
list_display = ('description', 'bill_link', 'rate', 'quantity', 'tax', 'subtotal')
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):
qset = super(BillLineManagerAdmin, self).get_queryset(request)
return qset.filter(bill_id__in=self.bill_ids)
if self.bill_ids:
return qset.filter(bill_id__in=self.bill_ids)
return qset
def changelist_view(self, request, extra_context=None):
GET = request.GET.copy()
bill_ids = GET.pop('bill_ids', ['0'])[0]
request.GET = GET
bill_ids = [int(id) for id in bill_ids.split(',')]
bill_ids = GET.pop('ids', None)
if bill_ids:
request.GET = GET
bill_ids = list(map(int, bill_ids.split(',')))
self.bill_ids = bill_ids
if not bill_ids:
return
elif len(bill_ids) > 1:
title = _("Manage bill lines of multiple bills.")
else:
if bill_ids and len(bill_ids) == 1:
bill_url = reverse('admin:bills_bill_change', args=(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)
else:
title = _("Manage bill lines of multiple bills.")
context = {
'title': title,
}
@ -147,8 +153,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
}),
)
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')
actions = [actions.download_bills, actions.close_bills, actions.send_bills]
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)
),
)
qs = qs.prefetch_related('transactions')
return qs
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(AmendmentFee, BillAdmin)
admin.site.register(ProForma, BillAdmin)
admin.site.register(BillLine, BillLineAdmin)
class BillContactInline(admin.StackedInline):

File diff suppressed because one or more lines are too long

View file

@ -290,17 +290,12 @@ class BillLine(models.Model):
related_name='amendment_lines', null=True, blank=True)
def __str__(self):
return "#%i" % self.number
@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()
return "#%i" % self.pk
def get_total(self):
""" Computes subline discounts """
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):
return self.verbose_quantity or self.quantity

View file

@ -17,11 +17,11 @@ class MySQLBackend(ServiceController):
if database.type != database.MYSQL:
return
context = self.get_context(database)
# Not available on delete()
context['owner'] = database.owner
self.append(
"mysql -e 'CREATE DATABASE `%(database)s`;' || true" % context
)
# Not available on delete()
context['owner'] = database.owner
# clean previous privileges
self.append("""mysql mysql -e 'DELETE FROM db WHERE db = "%(database)s";'""" % context)
for user in database.users.all():

View file

@ -15,7 +15,7 @@ class Database(models.Model):
name = models.CharField(_("name"), max_length=64, # MySQL limit
validators=[validators.validate_name])
users = models.ManyToManyField('databases.DatabaseUser',
users = models.ManyToManyField('databases.DatabaseUser', blank=True,
verbose_name=_("users"),related_name='databases')
type = models.CharField(_("type"), max_length=32,
choices=settings.DATABASES_TYPE_CHOICES,
@ -34,7 +34,10 @@ class Database(models.Model):
""" database owner is the first user related to it """
# Accessing intermediary model to get which is the first user
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 = (

View file

@ -89,7 +89,7 @@ class BillSelectedOrders(object):
url = change_url(bills[0])
else:
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
num = len(bills)
msg = ungettext(

View file

@ -10,7 +10,7 @@ from .models import Plan, ContractedPlan, Rate
class RateInline(admin.TabularInline):
model = Rate
ordering = ('plan', 'quantity')
ordering = ('service', 'plan', 'quantity')
class PlanAdmin(ExtendedModelAdmin):

View file

@ -12,7 +12,7 @@ def run_monitor(modeladmin, request, queryset):
for resource in queryset:
rlogs = resource.monitor()
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"))
if async:
num = len(queryset)

View file

@ -5,6 +5,7 @@ from orchestra.contrib.orchestration import ServiceController
from . import WebAppServiceMixin
# TODO DEPRECATE
class WebalizerAppBackend(WebAppServiceMixin, ServiceController):
""" Needed for cleaning up webalizer main folder when webapp deleteion withou related contents """
verbose_name = _("Webalizer App")

View file

@ -28,10 +28,11 @@ class WebalizerBackend(ServiceController):
def delete(self, 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:
self.append("rm -f %(webapp_path)s" % context)
if delete_webapp or not content.webapp.content_set.filter(website=content.website).exists():
self.append("rm -fr %(webapp_path)s" % context)
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 -f %(webalizer_conf_path)s" % context)

View file

@ -6,6 +6,7 @@ def html_to_pdf(html):
return run(
'PATH=$PATH:/usr/local/bin/\n'
'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')
).stdout