django-orchestra/orchestra/contrib/bills/admin.py

400 lines
16 KiB
Python
Raw Normal View History

2014-08-22 11:28:46 +00:00
from django import forms
2015-05-19 13:27:04 +00:00
from django.conf.urls import url
from django.contrib import admin, messages
2014-09-30 14:46:29 +00:00
from django.contrib.admin.utils import unquote
2014-07-23 16:24:56 +00:00
from django.core.urlresolvers import reverse
2014-09-03 13:56:02 +00:00
from django.db import models
2015-07-08 10:21:19 +00:00
from django.db.models import F, Sum, Prefetch
2015-04-14 14:29:22 +00:00
from django.db.models.functions import Coalesce
from django.templatetags.static import static
2014-09-30 14:46:29 +00:00
from django.utils.safestring import mark_safe
2014-07-23 16:24:56 +00:00
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import redirect
2014-07-23 16:24:56 +00:00
2014-08-19 18:59:23 +00:00
from orchestra.admin import ExtendedModelAdmin
2015-04-20 14:23:10 +00:00
from orchestra.admin.utils import admin_date, insertattr, admin_link
from orchestra.contrib.accounts.actions import list_accounts
2015-04-05 10:46:24 +00:00
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
2014-10-17 10:04:47 +00:00
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
2014-07-23 16:24:56 +00:00
2015-03-29 16:10:07 +00:00
from . import settings, actions
2015-07-08 10:21:19 +00:00
from .filters import (BillTypeListFilter, HasBillContactListFilter, TotalListFilter,
PaymentStateListFilter, AmendedListFilter)
from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine,
BillContact)
2014-07-23 16:24:56 +00:00
2014-09-18 15:07:39 +00:00
PAYMENT_STATE_COLORS = {
2015-07-02 10:49:44 +00:00
Bill.OPEN: 'grey',
Bill.CREATED: 'darkorange',
Bill.PROCESSED: 'darkorange',
Bill.AMENDED: 'blue',
2014-09-18 15:07:39 +00:00
Bill.PAID: 'green',
2015-07-02 10:49:44 +00:00
Bill.EXECUTED: 'darkorange',
2014-09-18 15:07:39 +00:00
Bill.BAD_DEBT: 'red',
2015-07-02 10:49:44 +00:00
Bill.INCOMPLETE: 'red',
2014-09-18 15:07:39 +00:00
}
2014-07-23 16:24:56 +00:00
class BillLineInline(admin.TabularInline):
model = BillLine
2015-04-20 14:23:10 +00:00
fields = (
'description', 'order_link', 'start_on', 'end_on', 'rate', 'quantity', 'tax',
'subtotal', 'display_total',
)
readonly_fields = ('display_total', 'order_link')
order_link = admin_link('order', display='pk')
2014-08-22 11:28:46 +00:00
def display_total(self, line):
2015-10-05 14:49:15 +00:00
if line.pk:
total = line.compute_total()
sublines = line.sublines.all()
if sublines:
content = '\n'.join(['%s: %s' % (sub.description, sub.total) for sub in sublines])
img = static('admin/img/icon_alert.gif')
return '<span title="%s">%s <img src="%s"></img></span>' % (content, total, img)
return total
display_total.short_description = _("Total")
display_total.allow_tags = True
2014-09-10 16:53:09 +00:00
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
if db_field.name == 'description':
2015-04-20 14:23:10 +00:00
kwargs['widget'] = forms.TextInput(attrs={'size':'50'})
elif db_field.name not in ('start_on', 'end_on'):
kwargs['widget'] = forms.TextInput(attrs={'size':'6'})
2016-02-23 11:49:10 +00:00
return super().formfield_for_dbfield(db_field, **kwargs)
def get_queryset(self, request):
2016-02-23 11:49:10 +00:00
qs = super().get_queryset(request)
2015-04-21 13:12:48 +00:00
return qs.prefetch_related('sublines').select_related('order')
class ClosedBillLineInline(BillLineInline):
# TODO reimplement as nested inlines when upstream
# https://code.djangoproject.com/ticket/9025
2015-04-14 14:29:22 +00:00
fields = (
2015-04-20 14:23:10 +00:00
'display_description', 'order_link', 'start_on', 'end_on', 'rate', 'quantity', 'tax',
'display_subtotal', 'display_total'
2015-04-14 14:29:22 +00:00
)
readonly_fields = fields
def display_description(self, line):
descriptions = [line.description]
for subline in line.sublines.all():
descriptions.append('&nbsp;'*4+subline.description)
return '<br>'.join(descriptions)
display_description.short_description = _("Description")
display_description.allow_tags = True
def display_subtotal(self, line):
2015-07-13 11:31:32 +00:00
subtotals = ['&nbsp;' + str(line.subtotal)]
for subline in line.sublines.all():
subtotals.append(str(subline.total))
return '<br>'.join(subtotals)
display_subtotal.short_description = _("Subtotal")
display_subtotal.allow_tags = True
2015-04-14 14:29:22 +00:00
def display_total(self, line):
2015-10-05 14:49:15 +00:00
if line.pk:
return line.compute_total()
2015-04-14 14:29:22 +00:00
display_total.short_description = _("Total")
display_total.allow_tags = True
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
2014-07-23 16:24:56 +00:00
2015-04-21 13:12:48 +00:00
class BillLineAdmin(admin.ModelAdmin):
list_display = (
2015-10-05 14:49:15 +00:00
'description', 'bill_link', 'display_is_open', 'account_link', 'rate', 'quantity',
'tax', 'subtotal', 'display_sublinetotal', 'display_total'
)
2015-07-13 11:31:32 +00:00
actions = (
actions.undo_billing, actions.move_lines, actions.copy_lines, actions.service_report
)
list_filter = ('tax', 'bill__is_open', 'order__service')
list_select_related = ('bill', 'bill__account')
search_fields = ('description', 'bill__number')
2015-03-29 16:10:07 +00:00
account_link = admin_link('bill__account')
2015-04-21 13:12:48 +00:00
bill_link = admin_link('bill')
def display_is_open(self, instance):
return instance.bill.is_open
display_is_open.short_description = _("Is open")
display_is_open.boolean = True
def display_sublinetotal(self, instance):
return instance.subline_total or ''
display_sublinetotal.short_description = _("Subline")
display_sublinetotal.admin_order_field = 'subline_total'
def display_total(self, instance):
return round(instance.computed_total or 0, 2)
display_total.short_description = _("Total")
display_total.admin_order_field = 'computed_total'
def get_queryset(self, request):
2016-02-23 11:49:10 +00:00
qs = super().get_queryset(request)
qs = qs.annotate(
subline_total=Sum('sublines__total'),
2015-07-13 11:31:32 +00:00
computed_total=(F('subtotal') + Sum(Coalesce('sublines__total', 0))) * (1+F('tax')/100),
)
return qs
2015-04-21 13:12:48 +00:00
class BillLineManagerAdmin(BillLineAdmin):
2015-03-29 16:10:07 +00:00
def get_queryset(self, request):
2016-02-23 11:49:10 +00:00
qset = super().get_queryset(request)
2015-04-21 13:12:48 +00:00
if self.bill_ids:
return qset.filter(bill_id__in=self.bill_ids)
return qset
2015-03-29 16:10:07 +00:00
def changelist_view(self, request, extra_context=None):
GET_copy = request.GET.copy()
bill_ids = GET_copy.pop('ids', None)
2015-04-21 13:12:48 +00:00
if bill_ids:
bill_ids = bill_ids[0]
request.GET = GET_copy
bill_ids = list(map(int, bill_ids.split(',')))
else:
messages.error(request, _("No bills selected."))
return redirect('..')
2015-03-29 16:10:07 +00:00
self.bill_ids = bill_ids
if len(bill_ids) == 1:
2015-03-29 16:10:07 +00:00
bill_url = reverse('admin:bills_bill_change', args=(bill_ids[0],))
bill = Bill.objects.get(pk=bill_ids[0])
2015-04-21 13:12:48 +00:00
bill_link = '<a href="%s">%s</a>' % (bill_url, bill.number)
2015-03-29 16:10:07 +00:00
title = mark_safe(_("Manage %s bill lines.") % bill_link)
if not bill.is_open:
messages.warning(request, _("Bill not in open state."))
2015-04-21 13:12:48 +00:00
else:
if Bill.objects.filter(id__in=bill_ids, is_open=False).exists():
messages.warning(request, _("Not all bills are in open state."))
2015-04-21 13:12:48 +00:00
title = _("Manage bill lines of multiple bills.")
2015-03-29 16:10:07 +00:00
context = {
'title': title,
}
context.update(extra_context or {})
2016-02-23 11:49:10 +00:00
return super().changelist_view(request, context)
2015-03-29 16:10:07 +00:00
2014-08-19 18:59:23 +00:00
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
2014-07-23 16:24:56 +00:00
list_display = (
2015-07-21 12:23:40 +00:00
'number', 'type_link', 'account_link', 'closed_on_display', 'updated_on_display',
'num_lines', 'display_total', 'display_payment_state', 'is_sent'
2014-07-23 16:24:56 +00:00
)
2015-07-08 10:21:19 +00:00
list_filter = (
BillTypeListFilter, 'is_open', 'is_sent', TotalListFilter, PaymentStateListFilter,
AmendedListFilter
)
add_fields = ('account', 'type', 'amend_of', 'is_open', 'due_on', 'comments')
2015-07-08 10:21:19 +00:00
change_list_template = 'admin/bills/change_list.html'
2014-08-22 11:28:46 +00:00
fieldsets = (
(None, {
2015-07-08 13:29:29 +00:00
'fields': ['number', 'type', 'amend_of_link', 'account_link', 'display_total',
2015-07-21 12:23:40 +00:00
'display_payment_state', 'is_sent', 'comments'],
}),
(_("Dates"), {
'classes': ('collapse',),
2015-10-05 14:49:15 +00:00
'fields': ('created_on_display', 'closed_on_display', 'updated_on_display',
'due_on'),
2014-08-22 11:28:46 +00:00
}),
(_("Raw"), {
'classes': ('collapse',),
'fields': ('html',),
}),
)
2015-07-13 11:31:32 +00:00
list_prefetch_related = ('transactions', 'lines__sublines')
search_fields = ('number', 'account__username', 'comments')
2015-03-29 16:10:07 +00:00
change_view_actions = [
2015-04-21 13:12:48 +00:00
actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills,
actions.close_bills, actions.amend_bills, actions.close_send_download_bills,
2015-03-29 16:10:07 +00:00
]
actions = [
2015-06-22 14:14:16 +00:00
actions.manage_lines, actions.download_bills, actions.close_bills, actions.send_bills,
2015-07-13 11:31:32 +00:00
actions.amend_bills, actions.bill_report, actions.service_report,
actions.close_send_download_bills, list_accounts,
]
2015-10-05 14:49:15 +00:00
change_readonly_fields = (
'account_link', 'type', 'is_open', 'amend_of_link', 'amend_links'
)
2015-07-21 12:23:40 +00:00
readonly_fields = (
'number', 'display_total', 'is_sent', 'display_payment_state', 'created_on_display',
'closed_on_display', 'updated_on_display'
)
inlines = [BillLineInline, ClosedBillLineInline]
2015-07-08 13:29:29 +00:00
date_hierarchy = 'closed_on'
2014-07-23 16:24:56 +00:00
2015-07-21 12:23:40 +00:00
created_on_display = admin_date('created_on', short_description=_("Created"))
closed_on_display = admin_date('closed_on', short_description=_("Closed"))
2015-07-10 13:00:51 +00:00
updated_on_display = admin_date('updated_on', short_description=_("Updated"))
amend_of_link = admin_link('amend_of')
2014-07-23 16:24:56 +00:00
2015-07-08 13:29:29 +00:00
def amend_links(self, bill):
links = []
for amend in bill.amends.all():
url = reverse('admin:bills_bill_change', args=(amend.id,))
links.append('<a href="{url}">{num}</a>'.format(url=url, num=amend.number))
return '<br>'.join(links)
amend_links.short_description = _("Amends")
amend_links.allow_tags = True
2014-09-03 13:56:02 +00:00
def num_lines(self, bill):
2014-09-11 14:00:20 +00:00
return bill.lines__count
num_lines.admin_order_field = 'lines__count'
2014-09-03 13:56:02 +00:00
num_lines.short_description = _("lines")
def display_total(self, bill):
2015-07-13 11:31:32 +00:00
return "%s &%s;" % (bill.compute_total(), settings.BILLS_CURRENCY.lower())
2014-09-03 13:56:02 +00:00
display_total.allow_tags = True
display_total.short_description = _("total")
2015-07-13 11:31:32 +00:00
display_total.admin_order_field = 'approx_total'
2014-09-03 13:56:02 +00:00
2014-08-19 18:59:23 +00:00
def type_link(self, bill):
bill_type = bill.type.lower()
2014-07-23 16:24:56 +00:00
url = reverse('admin:bills_%s_changelist' % bill_type)
2014-08-19 18:59:23 +00:00
return '<a href="%s">%s</a>' % (url, bill.get_type_display())
type_link.allow_tags = True
type_link.short_description = _("type")
type_link.admin_order_field = 'type'
2014-07-23 16:24:56 +00:00
2014-09-18 15:07:39 +00:00
def display_payment_state(self, bill):
2014-10-11 16:21:51 +00:00
t_opts = bill.transactions.model._meta
transactions = bill.transactions.all()
if len(transactions) == 1:
args = (transactions[0].pk,)
2015-10-05 14:49:15 +00:00
view = 'admin:%s_%s_change' % (t_opts.app_label, t_opts.model_name)
url = reverse(view, args=args)
2014-10-11 16:21:51 +00:00
else:
2015-04-03 13:03:08 +00:00
url = reverse('admin:%s_%s_changelist' % (t_opts.app_label, t_opts.model_name))
2014-10-11 16:21:51 +00:00
url += '?bill=%i' % bill.pk
2014-09-18 15:07:39 +00:00
state = bill.get_payment_state_display().upper()
2015-07-08 10:21:19 +00:00
title = ''
if bill.closed_amends:
state += '*'
title = _("This bill has been amended, this value may not be valid.")
2014-09-18 15:07:39 +00:00
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
2015-07-08 10:21:19 +00:00
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
url=url, color=color, name=state, title=title)
2014-09-18 15:07:39 +00:00
display_payment_state.allow_tags = True
display_payment_state.short_description = _("Payment")
2015-03-29 16:10:07 +00:00
def get_urls(self):
""" Hook bill lines management URLs on bill admin """
2016-02-23 11:49:10 +00:00
urls = super().get_urls()
2015-03-29 16:10:07 +00:00
admin_site = self.admin_site
2015-05-19 13:27:04 +00:00
extra_urls = [
2015-03-29 16:10:07 +00:00
url("^manage-lines/$",
admin_site.admin_view(BillLineManagerAdmin(BillLine, admin_site).changelist_view),
name='bills_bill_manage_lines'),
2015-05-19 13:27:04 +00:00
]
2015-03-29 16:10:07 +00:00
return extra_urls + urls
2014-08-22 11:28:46 +00:00
def get_readonly_fields(self, request, obj=None):
2016-02-23 11:49:10 +00:00
fields = super().get_readonly_fields(request, obj)
2014-09-18 15:07:39 +00:00
if obj and not obj.is_open:
2014-08-22 11:28:46 +00:00
fields += self.add_fields
return fields
2014-09-08 15:10:16 +00:00
def get_fieldsets(self, request, obj=None):
2016-02-23 11:49:10 +00:00
fieldsets = super().get_fieldsets(request, obj)
if obj:
2015-07-08 13:29:29 +00:00
# Switches between amend_of_link and amend_links fields
if obj.amend_of_id:
fieldsets[0][1]['fields'][2] = 'amend_of_link'
else:
fieldsets[0][1]['fields'][2] = 'amend_links'
if obj.is_open:
fieldsets = (fieldsets[0],)
2014-09-08 15:10:16 +00:00
return fieldsets
def get_change_view_actions(self, obj=None):
2016-02-23 11:49:10 +00:00
actions = super().get_change_view_actions(obj)
2014-09-18 15:07:39 +00:00
exclude = []
2014-09-04 15:55:43 +00:00
if obj:
2014-09-18 15:07:39 +00:00
if not obj.is_open:
exclude += ['close_bills', 'close_send_download_bills']
2016-02-11 14:24:09 +00:00
if obj.type not in obj.AMEND_MAP:
exclude += ['amend_bills']
2014-09-18 15:07:39 +00:00
return [action for action in actions if action.__name__ not in exclude]
2014-07-23 16:24:56 +00:00
def get_inline_instances(self, request, obj=None):
2016-02-23 11:49:10 +00:00
inlines = super().get_inline_instances(request, obj)
if obj and not obj.is_open:
2015-07-13 11:31:32 +00:00
return [inline for inline in inlines if type(inline) != BillLineInline]
return [inline for inline in inlines if type(inline) != ClosedBillLineInline]
2014-08-22 11:28:46 +00:00
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
if db_field.name == 'comments':
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
elif db_field.name == 'html':
2014-08-22 11:28:46 +00:00
kwargs['widget'] = forms.Textarea(attrs={'cols': 150, 'rows': 20})
2016-02-23 11:49:10 +00:00
formfield = super().formfield_for_dbfield(db_field, **kwargs)
if db_field.name == 'amend_of':
formfield.queryset = formfield.queryset.filter(is_open=False)
return formfield
def get_queryset(self, request):
2016-02-23 11:49:10 +00:00
qs = super().get_queryset(request)
2015-04-14 14:29:22 +00:00
qs = qs.annotate(
models.Count('lines'),
2015-07-13 11:31:32 +00:00
# FIXME https://code.djangoproject.com/ticket/10060
approx_total=Coalesce(Sum(
(F('lines__subtotal') + Coalesce('lines__sublines__total', 0)) * (1+F('lines__tax')/100),
), 0),
2015-04-14 14:29:22 +00:00
)
2015-07-09 10:44:22 +00:00
qs = qs.prefetch_related(
Prefetch('amends', queryset=Bill.objects.filter(is_open=False), to_attr='closed_amends')
)
2014-09-03 13:56:02 +00:00
return qs
2014-09-30 14:46:29 +00:00
def change_view(self, request, object_id, **kwargs):
# TODO raise404, here and everywhere
2014-10-11 16:21:51 +00:00
bill = self.get_object(request, unquote(object_id))
2015-03-29 16:10:07 +00:00
actions.validate_contact(request, bill, error=False)
2016-02-23 11:49:10 +00:00
return super().change_view(request, object_id, **kwargs)
2014-07-23 16:24:56 +00:00
admin.site.register(Bill, BillAdmin)
admin.site.register(Invoice, BillAdmin)
admin.site.register(AmendmentInvoice, BillAdmin)
admin.site.register(Fee, BillAdmin)
admin.site.register(AmendmentFee, BillAdmin)
2014-09-11 14:00:20 +00:00
admin.site.register(ProForma, BillAdmin)
2015-04-21 13:12:48 +00:00
admin.site.register(BillLine, BillLineAdmin)
2014-10-17 10:04:47 +00:00
class BillContactInline(admin.StackedInline):
model = BillContact
fields = ('name', 'address', ('city', 'zipcode'), 'country', 'vat')
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
2014-10-24 11:10:30 +00:00
if db_field.name == 'name':
kwargs['widget'] = forms.TextInput(attrs={'size':'90'})
2014-10-17 10:04:47 +00:00
if db_field.name == 'address':
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2})
if db_field.name == 'email_usage':
kwargs['widget'] = paddingCheckboxSelectMultiple(45)
2016-02-23 11:49:10 +00:00
return super().formfield_for_dbfield(db_field, **kwargs)
2014-10-17 10:04:47 +00:00
def has_bill_contact(account):
return hasattr(account, 'billcontact')
has_bill_contact.boolean = True
has_bill_contact.admin_order_field = 'billcontact'
insertattr(AccountAdmin, 'inlines', BillContactInline)
insertattr(AccountAdmin, 'list_display', has_bill_contact)
insertattr(AccountAdmin, 'list_filter', HasBillContactListFilter)
insertattr(AccountAdmin, 'list_select_related', 'billcontact')