2014-08-22 11:28:46 +00:00
|
|
|
from django import forms
|
2023-10-24 16:59:02 +00:00
|
|
|
from django.urls import re_path as url
|
2015-05-28 09:43:57 +00:00
|
|
|
from django.contrib import admin, messages
|
2014-09-30 14:46:29 +00:00
|
|
|
from django.contrib.admin.utils import unquote
|
2021-04-21 12:27:18 +00:00
|
|
|
from django.urls 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
|
2014-10-21 19:02:33 +00:00
|
|
|
from django.templatetags.static import static
|
2021-05-12 12:16:28 +00:00
|
|
|
from django.utils.html import format_html
|
2014-09-30 14:46:29 +00:00
|
|
|
from django.utils.safestring import mark_safe
|
2023-10-24 16:59:02 +00:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2015-05-28 09:43:57 +00:00
|
|
|
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
|
2016-04-06 19:00:16 +00:00
|
|
|
from orchestra.admin.utils import admin_date, insertattr, admin_link, change_url
|
2015-07-21 10:44:32 +00:00
|
|
|
from orchestra.contrib.accounts.actions import list_accounts
|
2015-04-05 10:46:24 +00:00
|
|
|
from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
|
2021-05-13 10:37:17 +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)
|
2021-01-13 15:41:36 +00:00
|
|
|
from .models import (Bill, Invoice, AmendmentInvoice, AbonoInvoice, Fee, AmendmentFee, ProForma, BillLine,
|
2016-04-06 19:00:16 +00:00
|
|
|
BillSubline, 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',
|
2016-06-17 10:00:04 +00:00
|
|
|
Bill.CREATED: 'magenta',
|
2015-07-02 10:49:44 +00:00
|
|
|
Bill.PROCESSED: 'darkorange',
|
|
|
|
Bill.AMENDED: 'blue',
|
2014-09-18 15:07:39 +00:00
|
|
|
Bill.PAID: 'green',
|
2016-06-17 10:00:04 +00:00
|
|
|
Bill.EXECUTED: 'olive',
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-04-06 19:00:16 +00:00
|
|
|
class BillSublineInline(admin.TabularInline):
|
|
|
|
model = BillSubline
|
|
|
|
fields = ('description', 'total', 'type')
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2016-04-06 19:00:16 +00:00
|
|
|
def get_readonly_fields(self, request, obj=None):
|
|
|
|
fields = super().get_readonly_fields(request, obj)
|
|
|
|
if obj and not obj.bill.is_open:
|
|
|
|
return self.get_fields(request)
|
|
|
|
return fields
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2016-04-06 19:00:16 +00:00
|
|
|
def get_max_num(self, request, obj=None):
|
|
|
|
if obj and not obj.bill.is_open:
|
|
|
|
return 0
|
|
|
|
return super().get_max_num(request, obj)
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2016-04-06 19:00:16 +00:00
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
|
|
if obj and not obj.bill.is_open:
|
|
|
|
return False
|
|
|
|
return super().has_delete_permission(request, obj)
|
|
|
|
|
|
|
|
|
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')
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2015-04-20 14:23:10 +00:00
|
|
|
order_link = admin_link('order', display='pk')
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("Total")
|
|
|
|
)
|
2021-05-12 12:16:28 +00:00
|
|
|
@mark_safe
|
2014-10-21 19:02:33 +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()
|
2016-04-06 19:00:16 +00:00
|
|
|
url = change_url(line)
|
2015-10-05 14:49:15 +00:00
|
|
|
if sublines:
|
|
|
|
content = '\n'.join(['%s: %s' % (sub.description, sub.total) for sub in sublines])
|
2016-05-03 12:14:59 +00:00
|
|
|
img = static('admin/img/icon-alert.svg')
|
2016-04-06 19:00:16 +00:00
|
|
|
return '<a href="%s" title="%s">%s <img src="%s"></img></a>' % (url, content, total, img)
|
|
|
|
return '<a href="%s">%s</a>' % (url, total)
|
2021-04-21 12:27:18 +00:00
|
|
|
|
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)
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2014-10-21 19:02:33 +00:00
|
|
|
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')
|
2014-10-21 19:02:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ClosedBillLineInline(BillLineInline):
|
|
|
|
# TODO reimplement as nested inlines when upstream
|
|
|
|
# https://code.djangoproject.com/ticket/9025
|
2021-04-21 12:27:18 +00:00
|
|
|
|
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
|
|
|
)
|
2014-10-21 19:02:33 +00:00
|
|
|
readonly_fields = fields
|
2016-05-18 14:08:12 +00:00
|
|
|
can_delete = False
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("Description")
|
|
|
|
)
|
2021-05-24 10:36:49 +00:00
|
|
|
@mark_safe
|
2014-10-21 19:02:33 +00:00
|
|
|
def display_description(self, line):
|
|
|
|
descriptions = [line.description]
|
|
|
|
for subline in line.sublines.all():
|
2021-05-24 10:36:49 +00:00
|
|
|
descriptions.append(' ' * 4 + subline.description)
|
2014-10-21 19:02:33 +00:00
|
|
|
return '<br>'.join(descriptions)
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("Subtotal")
|
|
|
|
)
|
2021-05-24 10:36:49 +00:00
|
|
|
@mark_safe
|
2014-10-21 19:02:33 +00:00
|
|
|
def display_subtotal(self, line):
|
2015-07-13 11:31:32 +00:00
|
|
|
subtotals = [' ' + str(line.subtotal)]
|
2014-10-21 19:02:33 +00:00
|
|
|
for subline in line.sublines.all():
|
|
|
|
subtotals.append(str(subline.total))
|
|
|
|
return '<br>'.join(subtotals)
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("Total")
|
|
|
|
)
|
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()
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2023-10-24 16:59:02 +00:00
|
|
|
def has_add_permission(self, request, obj):
|
2014-10-21 19:02:33 +00:00
|
|
|
return False
|
2014-07-29 14:29:59 +00:00
|
|
|
|
2014-07-23 16:24:56 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.register(BillLine)
|
2015-04-21 13:12:48 +00:00
|
|
|
class BillLineAdmin(admin.ModelAdmin):
|
2015-04-21 14:14:07 +00:00
|
|
|
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-04-21 14:14:07 +00:00
|
|
|
)
|
2015-07-13 11:31:32 +00:00
|
|
|
actions = (
|
2016-04-07 11:14:44 +00:00
|
|
|
actions.undo_billing, actions.move_lines, actions.copy_lines, actions.service_report,
|
|
|
|
actions.list_bills,
|
2015-07-13 11:31:32 +00:00
|
|
|
)
|
2016-04-06 19:00:16 +00:00
|
|
|
fieldsets = (
|
|
|
|
(None, {
|
|
|
|
'fields': ('bill_link', 'description', 'tax', 'start_on', 'end_on', 'amended_line_link')
|
|
|
|
}),
|
|
|
|
(_("Totals"), {
|
|
|
|
'fields': ('rate', ('quantity', 'verbose_quantity'), 'subtotal', 'display_sublinetotal',
|
|
|
|
'display_total'),
|
|
|
|
}),
|
|
|
|
(_("Order"), {
|
|
|
|
'fields': ('order_link', 'order_billed_on', 'order_billed_until',)
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
readonly_fields = (
|
|
|
|
'bill_link', 'order_link', 'amended_line_link', 'display_sublinetotal', 'display_total'
|
|
|
|
)
|
2015-07-13 11:31:32 +00:00
|
|
|
list_filter = ('tax', 'bill__is_open', 'order__service')
|
|
|
|
list_select_related = ('bill', 'bill__account')
|
2015-04-21 14:14:07 +00:00
|
|
|
search_fields = ('description', 'bill__number')
|
2016-04-06 19:00:16 +00:00
|
|
|
inlines = (BillSublineInline,)
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2015-05-28 09:43:57 +00:00
|
|
|
account_link = admin_link('bill__account')
|
2015-04-21 13:12:48 +00:00
|
|
|
bill_link = admin_link('bill')
|
2016-04-06 19:00:16 +00:00
|
|
|
order_link = admin_link('order')
|
|
|
|
amended_line_link = admin_link('amended_line')
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("Is open"),
|
|
|
|
boolean=True,
|
|
|
|
)
|
2015-05-28 09:43:57 +00:00
|
|
|
def display_is_open(self, instance):
|
|
|
|
return instance.bill.is_open
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("Sublines"),
|
|
|
|
ordering='subline_total',
|
|
|
|
)
|
2015-04-21 14:14:07 +00:00
|
|
|
def display_sublinetotal(self, instance):
|
2016-04-07 11:14:44 +00:00
|
|
|
total = instance.subline_total
|
|
|
|
return total if total is not None else '---'
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("Total"),
|
|
|
|
ordering='computed_total',
|
|
|
|
)
|
2015-04-21 14:14:07 +00:00
|
|
|
def display_total(self, instance):
|
|
|
|
return round(instance.computed_total or 0, 2)
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2016-04-06 19:00:16 +00:00
|
|
|
def get_readonly_fields(self, request, obj=None):
|
|
|
|
fields = super().get_readonly_fields(request, obj)
|
|
|
|
if obj and not obj.bill.is_open:
|
|
|
|
return list(fields) + [
|
|
|
|
'description', 'tax', 'start_on', 'end_on', 'rate', 'quantity', 'verbose_quantity',
|
|
|
|
'subtotal', 'order_billed_on', 'order_billed_until'
|
|
|
|
]
|
|
|
|
return fields
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2015-04-21 14:14:07 +00:00
|
|
|
def get_queryset(self, request):
|
2016-02-23 11:49:10 +00:00
|
|
|
qs = super().get_queryset(request)
|
2015-04-21 14:14:07 +00:00
|
|
|
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),
|
2015-04-21 14:14:07 +00:00
|
|
|
)
|
|
|
|
return qs
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2016-04-07 11:14:44 +00:00
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
|
|
if obj and not obj.bill.is_open:
|
|
|
|
return False
|
|
|
|
return super().has_delete_permission(request, obj)
|
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
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2015-03-29 16:10:07 +00:00
|
|
|
def changelist_view(self, request, extra_context=None):
|
2015-05-28 09:43:57 +00:00
|
|
|
GET_copy = request.GET.copy()
|
|
|
|
bill_ids = GET_copy.pop('ids', None)
|
2015-04-21 13:12:48 +00:00
|
|
|
if bill_ids:
|
2015-05-28 09:43:57 +00:00
|
|
|
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
|
2016-04-07 11:14:44 +00:00
|
|
|
bill = None
|
2015-05-28 09:43:57 +00:00
|
|
|
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)
|
2016-04-07 11:14:44 +00:00
|
|
|
title = mark_safe(_("Manage %s bill lines") % bill_link)
|
2015-05-28 09:43:57 +00:00
|
|
|
if not bill.is_open:
|
|
|
|
messages.warning(request, _("Bill not in open state."))
|
2015-04-21 13:12:48 +00:00
|
|
|
else:
|
2015-05-28 09:43:57 +00:00
|
|
|
if Bill.objects.filter(id__in=bill_ids, is_open=False).exists():
|
|
|
|
messages.warning(request, _("Not all bills are in open state."))
|
2016-04-07 11:14:44 +00:00
|
|
|
title = _("Manage bill lines of multiple bills")
|
2015-03-29 16:10:07 +00:00
|
|
|
context = {
|
|
|
|
'title': title,
|
2016-04-07 11:14:44 +00:00
|
|
|
'bill': bill,
|
2015-03-29 16:10:07 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
|
|
|
|
2016-05-18 14:08:12 +00:00
|
|
|
class BillAdminMixin(AccountAdminMixin):
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("total"),
|
|
|
|
ordering='approx_total',
|
|
|
|
)
|
2021-05-12 12:16:28 +00:00
|
|
|
@mark_safe
|
2016-05-18 14:08:12 +00:00
|
|
|
def display_total_with_subtotals(self, bill):
|
|
|
|
if bill.pk:
|
|
|
|
currency = settings.BILLS_CURRENCY.lower()
|
|
|
|
subtotals = []
|
|
|
|
for tax, subtotal in bill.compute_subtotals().items():
|
|
|
|
subtotals.append(_("Subtotal %s%% VAT %s &%s;") % (tax, subtotal[0], currency))
|
|
|
|
subtotals.append(_("Taxes %s%% VAT %s &%s;") % (tax, subtotal[1], currency))
|
|
|
|
subtotals = '\n'.join(subtotals)
|
|
|
|
return '<span title="%s">%s &%s;</span>' % (subtotals, bill.compute_total(), currency)
|
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("Payment")
|
|
|
|
)
|
2021-05-12 12:16:28 +00:00
|
|
|
@mark_safe
|
2016-05-18 14:08:12 +00:00
|
|
|
def display_payment_state(self, bill):
|
|
|
|
if bill.pk:
|
|
|
|
t_opts = bill.transactions.model._meta
|
|
|
|
if bill.get_type() == bill.PROFORMA:
|
|
|
|
return '<span title="Pro forma">---</span>'
|
|
|
|
transactions = bill.transactions.all()
|
|
|
|
if len(transactions) == 1:
|
|
|
|
args = (transactions[0].pk,)
|
|
|
|
view = 'admin:%s_%s_change' % (t_opts.app_label, t_opts.model_name)
|
|
|
|
url = reverse(view, 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()
|
|
|
|
title = ''
|
|
|
|
if bill.closed_amends:
|
|
|
|
state = '<strike>%s*</strike>' % state
|
|
|
|
title = _("This bill has been amended, this value may not be valid.")
|
|
|
|
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
|
|
|
return '<a href="{url}" style="color:{color}" title="{title}">{name}</a>'.format(
|
|
|
|
url=url, color=color, name=state, title=title)
|
|
|
|
|
|
|
|
def get_queryset(self, request):
|
|
|
|
qs = super().get_queryset(request)
|
|
|
|
qs = qs.annotate(
|
|
|
|
models.Count('lines'),
|
|
|
|
# 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),
|
|
|
|
)
|
|
|
|
qs = qs.prefetch_related(
|
|
|
|
Prefetch('amends', queryset=Bill.objects.filter(is_open=False), to_attr='closed_amends')
|
|
|
|
)
|
|
|
|
return qs.defer('html')
|
|
|
|
|
|
|
|
|
|
|
|
class AmendInline(BillAdminMixin, admin.TabularInline):
|
|
|
|
model = Bill
|
|
|
|
fields = (
|
|
|
|
'self_link', 'type', 'display_total_with_subtotals', 'display_payment_state', 'is_open',
|
|
|
|
'is_sent'
|
|
|
|
)
|
|
|
|
readonly_fields = fields
|
|
|
|
verbose_name_plural = _("Amends")
|
|
|
|
can_delete = False
|
|
|
|
extra = 0
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2016-05-18 14:08:12 +00:00
|
|
|
self_link = admin_link('__str__')
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2016-05-18 14:08:12 +00:00
|
|
|
def has_add_permission(self, *args, **kwargs):
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.register(AbonoInvoice, AmendmentFee, AmendmentInvoice, Bill, Fee, Invoice, ProForma)
|
2016-05-18 14:08:12 +00:00
|
|
|
class BillAdmin(BillAdminMixin, 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,
|
2016-06-17 10:00:04 +00:00
|
|
|
AmendedListFilter, 'account__is_active',
|
2015-07-08 10:21:19 +00:00
|
|
|
)
|
2015-07-07 10:41:34 +00:00
|
|
|
add_fields = ('account', 'type', 'amend_of', 'is_open', 'due_on', 'comments')
|
2016-04-07 11:14:44 +00:00
|
|
|
change_list_template = 'admin/bills/bill/change_list.html'
|
2014-08-22 11:28:46 +00:00
|
|
|
fieldsets = (
|
|
|
|
(None, {
|
2016-05-18 14:08:12 +00:00
|
|
|
'fields': ['number', 'type', (), 'account_link', 'display_total_with_subtotals',
|
|
|
|
'display_payment_state', 'is_sent', 'comments'],
|
2015-07-21 12:23:40 +00:00
|
|
|
}),
|
|
|
|
(_("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')
|
2015-05-28 09:43:57 +00:00
|
|
|
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,
|
2015-07-09 10:19:30 +00:00
|
|
|
actions.close_bills, actions.amend_bills, actions.close_send_download_bills,
|
2015-03-29 16:10:07 +00:00
|
|
|
]
|
2015-05-28 09:43:57 +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,
|
2016-04-07 11:14:44 +00:00
|
|
|
actions.close_send_download_bills, list_accounts,
|
2015-05-28 09:43:57 +00:00
|
|
|
]
|
2016-05-18 14:08:12 +00:00
|
|
|
change_readonly_fields = ('account_link', 'type', 'is_open', 'amend_of_link')
|
2015-07-21 12:23:40 +00:00
|
|
|
readonly_fields = (
|
|
|
|
'number', 'display_total', 'is_sent', 'display_payment_state', 'created_on_display',
|
2016-03-04 09:46:39 +00:00
|
|
|
'closed_on_display', 'updated_on_display', 'display_total_with_subtotals',
|
2015-07-21 12:23:40 +00:00
|
|
|
)
|
2016-04-15 09:56:10 +00:00
|
|
|
date_hierarchy = 'closed_on'
|
2021-04-21 12:27:18 +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"))
|
2015-07-07 10:41:34 +00:00
|
|
|
amend_of_link = admin_link('amend_of')
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2016-05-18 14:08:12 +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
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("lines"),
|
|
|
|
ordering='lines__count',
|
|
|
|
)
|
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
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("total"),
|
|
|
|
ordering='approx_total',
|
|
|
|
)
|
2014-09-03 13:56:02 +00:00
|
|
|
def display_total(self, bill):
|
2016-03-04 09:46:39 +00:00
|
|
|
currency = settings.BILLS_CURRENCY.lower()
|
2021-05-12 12:16:28 +00:00
|
|
|
return format_html('{} &{};', bill.compute_total(), currency)
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
description=_("type"),
|
|
|
|
ordering='type',
|
|
|
|
)
|
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)
|
2021-05-12 12:16:28 +00:00
|
|
|
return format_html('<a href="{}">{}</a>', url, bill.get_type_display())
|
2021-04-21 12:27:18 +00:00
|
|
|
|
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
|
2021-04-21 12:27:18 +00:00
|
|
|
|
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
|
2021-04-21 12:27:18 +00:00
|
|
|
|
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)
|
2015-07-07 10:41:34 +00:00
|
|
|
if obj:
|
2015-07-08 13:29:29 +00:00
|
|
|
# Switches between amend_of_link and amend_links fields
|
2016-05-18 14:08:12 +00:00
|
|
|
fields = fieldsets[0][1]['fields']
|
2015-07-08 13:29:29 +00:00
|
|
|
if obj.amend_of_id:
|
2016-05-18 14:08:12 +00:00
|
|
|
fields[2] = 'amend_of_link'
|
2015-07-08 13:29:29 +00:00
|
|
|
else:
|
2016-05-18 14:08:12 +00:00
|
|
|
fields[2] = ()
|
2015-07-07 10:41:34 +00:00
|
|
|
if obj.is_open:
|
2016-04-06 19:00:16 +00:00
|
|
|
fieldsets = fieldsets[0:-1]
|
2014-09-08 15:10:16 +00:00
|
|
|
return fieldsets
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2014-09-03 22:01:44 +00:00
|
|
|
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:
|
2015-07-09 10:19:30 +00:00
|
|
|
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]
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2014-07-23 16:24:56 +00:00
|
|
|
def get_inline_instances(self, request, obj=None):
|
2016-05-18 14:08:12 +00:00
|
|
|
cls = type(self)
|
2014-10-21 19:02:33 +00:00
|
|
|
if obj and not obj.is_open:
|
2016-05-18 14:08:12 +00:00
|
|
|
if obj.amends.all():
|
|
|
|
cls.inlines = [AmendInline, ClosedBillLineInline]
|
|
|
|
else:
|
|
|
|
cls.inlines = [ClosedBillLineInline]
|
|
|
|
else:
|
|
|
|
cls.inlines = [BillLineInline]
|
|
|
|
return super().get_inline_instances(request, obj)
|
2021-04-21 12:27:18 +00:00
|
|
|
|
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})
|
2015-07-07 10:41:34 +00:00
|
|
|
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)
|
2015-07-07 10:41:34 +00:00
|
|
|
if db_field.name == 'amend_of':
|
|
|
|
formfield.queryset = formfield.queryset.filter(is_open=False)
|
|
|
|
return formfield
|
2021-04-21 12:27:18 +00:00
|
|
|
|
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-09-03 22:01:44 +00:00
|
|
|
|
2014-07-23 16:24:56 +00:00
|
|
|
|
2014-10-17 10:04:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
class BillContactInline(admin.StackedInline):
|
|
|
|
model = BillContact
|
|
|
|
fields = ('name', 'address', ('city', 'zipcode'), 'country', 'vat')
|
2021-04-21 12:27:18 +00:00
|
|
|
|
2014-10-17 10:04:47 +00:00
|
|
|
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':
|
2015-07-23 12:41:42 +00:00
|
|
|
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':
|
2021-05-13 10:37:17 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
2024-01-26 13:05:02 +00:00
|
|
|
@admin.display(
|
|
|
|
boolean=True,
|
|
|
|
ordering='billcontact',
|
|
|
|
)
|
2014-10-17 10:04:47 +00:00
|
|
|
def has_bill_contact(account):
|
|
|
|
return hasattr(account, 'billcontact')
|
|
|
|
|
|
|
|
|
|
|
|
insertattr(AccountAdmin, 'inlines', BillContactInline)
|
|
|
|
insertattr(AccountAdmin, 'list_display', has_bill_contact)
|
|
|
|
insertattr(AccountAdmin, 'list_filter', HasBillContactListFilter)
|
2015-07-23 12:41:42 +00:00
|
|
|
insertattr(AccountAdmin, 'list_select_related', 'billcontact')
|