diff --git a/TODO.md b/TODO.md index a5e52152..da145c6b 100644 --- a/TODO.md +++ b/TODO.md @@ -430,7 +430,6 @@ mkhomedir_helper or create ssh homes with bash.rc and such # Automatically re-run backends until success? only timedout executions? # TODO save serialized versions ob backendoperation.instance in order to allow backend reexecution of deleted objects -# upgrade to django 1.9 and make margins wider # lets encrypt: DNS vs HTTP challange # lets enctypt: autorenew @@ -459,8 +458,7 @@ with open(file) as handler: os.unlink(file) -# change filter By PHP version: by detail - # Mark transaction process as executed should not override higher transaction states +# Bill amend and related transaction, what to do? allow edit transaction ammount of amends when their are pending execution -# mailbox.addresses get_Queryset SQL contact @ with mailboxes and forwards +# DASHBOARD: Show owned tickets, scheduled actions, maintenance operations (diff domains) diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index deee5194..f49be37d 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -142,6 +142,7 @@ class ChangeViewActionsMixin(object): view.tool_description = tool_description view.css_class = getattr(action, 'css_class', 'historylink') view.help_text = getattr(action, 'help_text', '') + view.hidden = getattr(action, 'hidden', False) views.append(view) return views @@ -150,7 +151,7 @@ class ChangeViewActionsMixin(object): kwargs['extra_context'] = {} obj = self.get_object(request, unquote(object_id)) kwargs['extra_context']['object_tools_items'] = [ - action.__dict__ for action in self.get_change_view_actions(obj) + action.__dict__ for action in self.get_change_view_actions(obj) if not action.hidden ] return super().change_view(request, object_id, **kwargs) diff --git a/orchestra/contrib/bills/actions.py b/orchestra/contrib/bills/actions.py index f288b4b0..058d66fa 100644 --- a/orchestra/contrib/bills/actions.py +++ b/orchestra/contrib/bills/actions.py @@ -32,6 +32,7 @@ def view_bill(modeladmin, request, queryset): return HttpResponse(html) view_bill.tool_description = _("View") view_bill.url_name = 'view' +view_bill.hidden = True @transaction.atomic diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index d4604b39..55bd3569 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -102,6 +102,7 @@ class ClosedBillLineInline(BillLineInline): 'display_subtotal', 'display_total' ) readonly_fields = fields + can_delete = False def display_description(self, line): descriptions = [line.description] @@ -127,9 +128,6 @@ class ClosedBillLineInline(BillLineInline): def has_add_permission(self, request): return False - - def has_delete_permission(self, request, obj=None): - return False class BillLineAdmin(admin.ModelAdmin): @@ -243,7 +241,77 @@ class BillLineManagerAdmin(BillLineAdmin): return super().changelist_view(request, context) -class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): +class BillAdminMixin(AccountAdminMixin): + 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 '%s &%s;' % (subtotals, bill.compute_total(), currency) + display_total_with_subtotals.allow_tags = True + display_total_with_subtotals.short_description = _("total") + display_total_with_subtotals.admin_order_field = 'approx_total' + + def display_payment_state(self, bill): + if bill.pk: + t_opts = bill.transactions.model._meta + if bill.get_type() == bill.PROFORMA: + return '---' + 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 = '%s*' % state + title = _("This bill has been amended, this value may not be valid.") + color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey') + return '{name}'.format( + url=url, color=color, name=state, title=title) + display_payment_state.allow_tags = True + display_payment_state.short_description = _("Payment") + + 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 + + self_link = admin_link('__str__') + + def has_add_permission(self, *args, **kwargs): + return False + + +class BillAdmin(BillAdminMixin, ExtendedModelAdmin): list_display = ( 'number', 'type_link', 'account_link', 'closed_on_display', 'updated_on_display', 'num_lines', 'display_total', 'display_payment_state', 'is_sent' @@ -256,9 +324,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): change_list_template = 'admin/bills/bill/change_list.html' fieldsets = ( (None, { - 'fields': ['number', 'type', 'amend_of_link', 'account_link', - 'display_total_with_subtotals', 'display_payment_state', - 'is_sent', 'comments'], + 'fields': ['number', 'type', (), 'account_link', 'display_total_with_subtotals', + 'display_payment_state', 'is_sent', 'comments'], }), (_("Dates"), { 'classes': ('collapse',), @@ -281,31 +348,26 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): actions.amend_bills, actions.bill_report, actions.service_report, actions.close_send_download_bills, list_accounts, ] - change_readonly_fields = ( - 'account_link', 'type', 'is_open', 'amend_of_link', 'amend_links' - ) + change_readonly_fields = ('account_link', 'type', 'is_open', 'amend_of_link') readonly_fields = ( 'number', 'display_total', 'is_sent', 'display_payment_state', 'created_on_display', 'closed_on_display', 'updated_on_display', 'display_total_with_subtotals', ) - inlines = [BillLineInline, ClosedBillLineInline] date_hierarchy = 'closed_on' - # TODO when merged https://github.com/django/django/pull/5213 - #approximate_date_hierarchy = admin.ApproximateWith.MONTHS created_on_display = admin_date('created_on', short_description=_("Created")) closed_on_display = admin_date('closed_on', short_description=_("Closed")) updated_on_display = admin_date('updated_on', short_description=_("Updated")) amend_of_link = admin_link('amend_of') - def amend_links(self, bill): - links = [] - for amend in bill.amends.all(): - url = reverse('admin:bills_bill_change', args=(amend.id,)) - links.append('{num}'.format(url=url, num=amend.number)) - return '
'.join(links) - amend_links.short_description = _("Amends") - amend_links.allow_tags = True +# def amend_links(self, bill): +# links = [] +# for amend in bill.amends.all(): +# url = reverse('admin:bills_bill_change', args=(amend.id,)) +# links.append('{num}'.format(url=url, num=amend.number)) +# return '
'.join(links) +# amend_links.short_description = _("Amends") +# amend_links.allow_tags = True def num_lines(self, bill): return bill.lines__count @@ -319,18 +381,6 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): display_total.short_description = _("total") display_total.admin_order_field = 'approx_total' - def display_total_with_subtotals(self, bill): - 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 '%s &%s;' % (subtotals, bill.compute_total(), currency) - display_total_with_subtotals.allow_tags = True - display_total_with_subtotals.short_description = _("total") - display_total_with_subtotals.admin_order_field = 'approx_total' - def type_link(self, bill): bill_type = bill.type.lower() url = reverse('admin:bills_%s_changelist' % bill_type) @@ -339,27 +389,6 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): 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,) - 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 = '%s*' % state - title = _("This bill has been amended, this value may not be valid.") - color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey') - return '{name}'.format( - url=url, color=color, name=state, title=title) - 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().get_urls() @@ -381,10 +410,11 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): fieldsets = super().get_fieldsets(request, obj) if obj: # Switches between amend_of_link and amend_links fields + fields = fieldsets[0][1]['fields'] if obj.amend_of_id: - fieldsets[0][1]['fields'][2] = 'amend_of_link' + fields[2] = 'amend_of_link' else: - fieldsets[0][1]['fields'][2] = 'amend_links' + fields[2] = () if obj.is_open: fieldsets = fieldsets[0:-1] return fieldsets @@ -400,10 +430,15 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): return [action for action in actions if action.__name__ not in exclude] def get_inline_instances(self, request, obj=None): - inlines = super().get_inline_instances(request, obj) + cls = type(self) if obj and not obj.is_open: - return [inline for inline in inlines if type(inline) != BillLineInline] - return [inline for inline in inlines if type(inline) != ClosedBillLineInline] + if obj.amends.all(): + cls.inlines = [AmendInline, ClosedBillLineInline] + else: + cls.inlines = [ClosedBillLineInline] + else: + cls.inlines = [BillLineInline] + return super().get_inline_instances(request, obj) def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ @@ -416,20 +451,6 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): formfield.queryset = formfield.queryset.filter(is_open=False) return formfield - 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') - def change_view(self, request, object_id, **kwargs): # TODO raise404, here and everywhere bill = self.get_object(request, unquote(object_id)) diff --git a/orchestra/contrib/bills/filters.py b/orchestra/contrib/bills/filters.py index 091c49c8..27a4b239 100644 --- a/orchestra/contrib/bills/filters.py +++ b/orchestra/contrib/bills/filters.py @@ -140,20 +140,21 @@ class AmendedListFilter(SimpleListFilter): def lookups(self, request, model_admin): return ( - ('1', _("Closed amends")), - ('-1', _("Open or closed amends")), - ('0', _("No closed amends")), - ('-0', _("No amends")), + ('1', _("Amended")), + ('2', _("Open amends")), + ('3', _("Closed amends")), + ('0', _("No amends")), ) def queryset(self, request, queryset): if self.value() is None: return queryset - amended = queryset.filter(type__in=Bill.AMEND_MAP.values(), amend_of__isnull=False) - if not self.value().startswith('-'): - amended = amended.filter(is_open=False) - amended_ids = amended.distinct().values_list('amend_of_id', flat=True) - if self.value().endswith('1'): - return queryset.filter(id__in=amended_ids) - else: - return queryset.exclude(id__in=amended_ids) + amended = queryset.filter(amends__isnull=False) + if self.value() == '1': + return amended.distinct() + elif self.value() == '2': + return amended.filter(amends__is_open=True).distinct() + elif self.value() == '3': + return amended.filter(amends__is_open=False).distinct() + elif self.value() == '0': + return queryset.filter(amends__isnull=True).distinct() diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index 3b29356d..5a6453ed 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -1,6 +1,7 @@ import datetime from dateutil.relativedelta import relativedelta +from django.core.urlresolvers import reverse from django.core.validators import ValidationError, RegexValidator from django.db import models from django.db.models import F, Sum @@ -254,6 +255,9 @@ class Bill(models.Model): return now + payment.get_due_delta() return now + relativedelta(months=1) + def get_absolute_url(self): + return reverse('admin:bills_bill_view', args=(self.pk,)) + def close(self, payment=False): if not self.is_open: raise TypeError("Bill not in Open state.") diff --git a/orchestra/contrib/bills/templates/bills/microspective-proforma.html b/orchestra/contrib/bills/templates/bills/microspective-proforma.html index 8efbfbd3..92400244 100644 --- a/orchestra/contrib/bills/templates/bills/microspective-proforma.html +++ b/orchestra/contrib/bills/templates/bills/microspective-proforma.html @@ -7,3 +7,7 @@ {% endwith %} {% endblock %} + + +{% block payment %} +{% endblock %} diff --git a/orchestra/contrib/bills/templates/bills/microspective.html b/orchestra/contrib/bills/templates/bills/microspective.html index 52091115..6992d456 100644 --- a/orchestra/contrib/bills/templates/bills/microspective.html +++ b/orchestra/contrib/bills/templates/bills/microspective.html @@ -140,6 +140,7 @@ diff --git a/orchestra/contrib/payments/admin.py b/orchestra/contrib/payments/admin.py index 7814d1d7..74041bb8 100644 --- a/orchestra/contrib/payments/admin.py +++ b/orchestra/contrib/payments/admin.py @@ -109,7 +109,6 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): search_fields = ('bill__number', 'bill__account__username', 'id') actions = change_view_actions + (actions.report, list_accounts,) filter_by_account_fields = ('bill', 'source') - change_readonly_fields = ('amount', 'currency') readonly_fields = ( 'bill_link', 'display_state', 'process_link', 'account_link', 'source_link', 'display_created_at', 'display_modified_at' @@ -133,6 +132,11 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): del actions['delete_selected'] return actions + def get_change_readonly_fields(self, request, obj): + if obj.state in (Transaction.WAITTING_PROCESSING, Transaction.WAITTING_EXECUTION): + return () + return ('amount', 'currency') + def get_change_view_actions(self, obj=None): actions = super(TransactionAdmin, self).get_change_view_actions() exclude = [] diff --git a/orchestra/contrib/services/templates/admin/services/service/update_orders.html b/orchestra/contrib/services/templates/admin/services/service/update_orders.html index ea171f09..01882218 100644 --- a/orchestra/contrib/services/templates/admin/services/service/update_orders.html +++ b/orchestra/contrib/services/templates/admin/services/service/update_orders.html @@ -10,21 +10,21 @@ - - - - - - - - + + + + + + + + + {% for order, action in updates %} - + @@ -32,6 +32,7 @@ + {% endfor %} diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py index 9b1d5099..d126f807 100644 --- a/orchestra/contrib/systemusers/backends.py +++ b/orchestra/contrib/systemusers/backends.py @@ -262,7 +262,7 @@ class UNIXUserDisk(ServiceMonitor): super(UNIXUserDisk, self).prepare() self.append(textwrap.dedent("""\ function monitor () { - { du -bs "$1" || echo 0; } | awk {'print $1'} + { SIZE=$(du -bs "$1") && echo $SIZE || echo 0; } | awk {'print $1'} }""" )) diff --git a/orchestra/contrib/webapps/filters.py b/orchestra/contrib/webapps/filters.py index bbf1cce3..a79cd228 100644 --- a/orchestra/contrib/webapps/filters.py +++ b/orchestra/contrib/webapps/filters.py @@ -27,7 +27,7 @@ class DetailListFilter(SimpleListFilter): parameter_name = 'detail' def lookups(self, request, model_admin): - ret = set() + ret = set([('empty', _("Empty"))]) lookup_map = {} for apptype in AppType.get_plugins(): for field, values in apptype.get_detail_lookups().items(): @@ -35,11 +35,13 @@ class DetailListFilter(SimpleListFilter): lookup_map[value[0]] = field ret.add(value) self.lookup_map = lookup_map - return sorted(list(ret)) + return sorted(list(ret), key=lambda e: e[1]) def queryset(self, request, queryset): value = self.value() if value: + if value == 'empty': + return queryset.filter(data={}) try: field = self.lookup_map[value] except KeyError:
Action
ID
Service
Account
Content object
Registered on
Billed until
Cancelled on -
Action
ID
Service
Account
Content object
Registered on
Billed until
Cancelled on
Ignored
{{ action }}{{ action.capitalize }} {% if order.pk %}{{ order.id }}{% endif %} {{ order.service }} {{ order.account }}{{ order.registered_on }} {{ order.billed_unitl }} {{ order.canncelled_on }}