from django import forms from django.conf.urls import patterns, url 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, Prefetch from django.db.models.functions import Coalesce from django.templatetags.static import static from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import admin_date, insertattr, admin_link from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin 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, BillSubline, BillContact PAYMENT_STATE_COLORS = { Bill.PAID: 'green', Bill.PENDING: 'darkorange', Bill.BAD_DEBT: 'red', } class BillLineInline(admin.TabularInline): model = BillLine 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') def display_total(self, line): total = line.get_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 '%s ' % (content, str(total), img) return total display_total.short_description = _("Total") display_total.allow_tags = True def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ if db_field.name == 'description': kwargs['widget'] = forms.TextInput(attrs={'size':'50'}) elif db_field.name not in ('start_on', 'end_on'): kwargs['widget'] = forms.TextInput(attrs={'size':'6'}) return super(BillLineInline, self).formfield_for_dbfield(db_field, **kwargs) def get_queryset(self, request): qs = super(BillLineInline, self).get_queryset(request) return qs.prefetch_related('sublines').select_related('order') class ClosedBillLineInline(BillLineInline): # TODO reimplement as nested inlines when upstream # https://code.djangoproject.com/ticket/9025 fields = ( 'display_description', 'order_link', 'start_on', 'end_on', 'rate', 'quantity', 'tax', 'display_subtotal', 'display_total' ) readonly_fields = fields def display_description(self, line): descriptions = [line.description] for subline in line.sublines.all(): descriptions.append(' '*4+subline.description) return '
'.join(descriptions) display_description.short_description = _("Description") display_description.allow_tags = True def display_subtotal(self, line): subtotals = ['  ' + str(line.subtotal)] for subline in line.sublines.all(): subtotals.append(str(subline.total)) return '
'.join(subtotals) display_subtotal.short_description = _("Subtotal") display_subtotal.allow_tags = True def display_total(self, line): return line.get_total() 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 class BillLineAdmin(admin.ModelAdmin): list_display = ( 'description', 'bill_link', 'rate', 'quantity', 'tax', 'subtotal', 'display_sublinetotal', 'display_total' ) actions = (actions.undo_billing, actions.move_lines, actions.copy_lines,) list_filter = ('tax', ('bill', admin.RelatedOnlyFieldListFilter)) list_select_related = ('bill',) search_fields = ('description', 'bill__number') bill_link = admin_link('bill') 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): qs = super(BillLineAdmin, self).get_queryset(request) qs = qs.annotate( subline_total=Sum('sublines__total'), computed_total=Sum( (F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100) ), ) return qs class BillLineManagerAdmin(BillLineAdmin): def get_queryset(self, request): qset = super(BillLineManagerAdmin, self).get_queryset(request) 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('ids', None) if bill_ids: request.GET = GET bill_ids = list(map(int, bill_ids)) self.bill_ids = bill_ids 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 = '%s' % (bill_url, bill.number) title = mark_safe(_("Manage %s bill lines.") % bill_link) else: title = _("Manage bill lines of multiple bills.") context = { 'title': title, } context.update(extra_context or {}) return super(BillLineManagerAdmin, self).changelist_view(request, context) class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): list_display = ( 'number', 'type_link', 'account_link', 'created_on_display', 'num_lines', 'display_total', 'display_payment_state', 'is_open', 'is_sent' ) list_filter = (BillTypeListFilter, 'is_open', 'is_sent') add_fields = ('account', 'type', 'is_open', 'due_on', 'comments') fieldsets = ( (None, { 'fields': ('number', 'type', 'account_link', 'display_total', 'display_payment_state', 'is_sent', 'due_on', 'comments'), }), (_("Raw"), { 'classes': ('collapse',), 'fields': ('html',), }), ) change_view_actions = [ 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') readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state') inlines = [BillLineInline, ClosedBillLineInline] created_on_display = admin_date('created_on') def num_lines(self, bill): return bill.lines__count num_lines.admin_order_field = 'lines__count' num_lines.short_description = _("lines") def display_total(self, bill): return "%s &%s;" % (round(bill.computed_total or 0, 2), settings.BILLS_CURRENCY.lower()) display_total.allow_tags = True display_total.short_description = _("total") display_total.admin_order_field = 'computed_total' def type_link(self, bill): bill_type = bill.type.lower() url = reverse('admin:bills_%s_changelist' % bill_type) return '%s' % (url, bill.get_type_display()) type_link.allow_tags = True type_link.short_description = _("type") type_link.admin_order_field = 'type' def display_payment_state(self, bill): t_opts = bill.transactions.model._meta transactions = bill.transactions.all() if len(transactions) == 1: args = (transactions[0].pk,) url = reverse('admin:%s_%s_change' % (t_opts.app_label, t_opts.model_name), args=args) else: url = reverse('admin:%s_%s_changelist' % (t_opts.app_label, t_opts.model_name)) url += '?bill=%i' % bill.pk state = bill.get_payment_state_display().upper() color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey') return '{name}'.format( url=url, color=color, name=state) display_payment_state.allow_tags = True display_payment_state.short_description = _("Payment") def get_urls(self): """ Hook bill lines management URLs on bill admin """ urls = super(BillAdmin, self).get_urls() admin_site = self.admin_site extra_urls = patterns("", url("^manage-lines/$", admin_site.admin_view(BillLineManagerAdmin(BillLine, admin_site).changelist_view), name='bills_bill_manage_lines'), ) return extra_urls + urls def get_readonly_fields(self, request, obj=None): fields = super(BillAdmin, self).get_readonly_fields(request, obj) if obj and not obj.is_open: fields += self.add_fields return fields def get_fieldsets(self, request, obj=None): fieldsets = super(BillAdmin, self).get_fieldsets(request, obj) if obj and obj.is_open: fieldsets = (fieldsets[0],) return fieldsets def get_change_view_actions(self, obj=None): actions = super(BillAdmin, self).get_change_view_actions(obj) exclude = [] if obj: if not obj.is_open: exclude.append('close_bills') return [action for action in actions if action.__name__ not in exclude] def get_inline_instances(self, request, obj=None): inlines = super(BillAdmin, self).get_inline_instances(request, obj) if obj and not obj.is_open: return [inline for inline in inlines if not isinstance(inline, BillLineInline)] return [inline for inline in inlines if not isinstance(inline, ClosedBillLineInline)] 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}) if db_field.name == 'html': kwargs['widget'] = forms.Textarea(attrs={'cols': 150, 'rows': 20}) return super(BillAdmin, self).formfield_for_dbfield(db_field, **kwargs) def get_queryset(self, request): qs = super(BillAdmin, self).get_queryset(request) qs = qs.annotate( models.Count('lines'), computed_total=Sum( (F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100) ), ) return qs def change_view(self, request, object_id, **kwargs): # TODO raise404, here and everywhere bill = self.get_object(request, unquote(object_id)) actions.validate_contact(request, bill, error=False) return super(BillAdmin, self).change_view(request, object_id, **kwargs) 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) admin.site.register(ProForma, BillAdmin) admin.site.register(BillLine, BillLineAdmin) 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 """ if db_field.name == 'name': kwargs['widget'] = forms.TextInput(attrs={'size':'70'}) if db_field.name == 'address': kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2}) if db_field.name == 'email_usage': kwargs['widget'] = paddingCheckboxSelectMultiple(45) return super(BillContactInline, self).formfield_for_dbfield(db_field, **kwargs) 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)