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? # 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???

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.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

View File

@ -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:

View File

@ -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)
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): 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)
request.GET = GET if bill_ids:
bill_ids = [int(id) for id in bill_ids.split(',')] request.GET = GET
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

View File

@ -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

View File

@ -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():

View File

@ -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 = (

View File

@ -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(

View File

@ -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):

View File

@ -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)

View File

@ -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")

View File

@ -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)

View File

@ -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