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?
|
||||
* 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???
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue