from django.contrib import admin, messages from django.forms import Textarea from django.http import HttpResponse, HttpResponseRedirect from django.db.models import TextField, Q from django.urls import path, reverse from functools import update_wrapper from django.utils.translation import gettext_lazy as _ from django.shortcuts import render from django.conf import settings from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin from utils import get_member from rules.contrib.admin import ObjectPermissionsModelAdmin from .models import Ledger, Statement, Receipt, Transaction, Bill, StatementSubmitted, StatementConfirmed,\ StatementUnSubmitted, BillOnStatementProxy @admin.register(Ledger) class LedgerAdmin(admin.ModelAdmin): search_fields = ('name', ) class BillOnStatementInline(CommonAdminInlineMixin, admin.TabularInline): model = BillOnStatementProxy extra = 0 sortable_options = [] fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof'] formfield_overrides = { TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} } @admin.register(StatementUnSubmitted) class StatementUnSubmittedAdmin(CommonAdminMixin, admin.ModelAdmin): fields = ['short_description', 'explanation', 'excursion', 'submitted'] list_display = ['__str__', 'excursion', 'created_by'] inlines = [BillOnStatementInline] def save_model(self, request, obj, form, change): if not change and hasattr(request.user, 'member'): obj.created_by = request.user.member super().save_model(request, obj, form, change) def get_readonly_fields(self, request, obj=None): readonly_fields = ['submitted', 'excursion'] if obj is not None and obj.submitted: return readonly_fields + self.fields else: return readonly_fields def get_urls(self): urls = super().get_urls() def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) wrapper.model_admin = self return update_wrapper(wrapper, view) custom_urls = [ path( "/submit/", wrap(self.submit_view), name="%s_%s_submit" % (self.opts.app_label, self.opts.model_name), ), ] return custom_urls + urls def submit_view(self, request, object_id): statement = Statement.objects.get(pk=object_id) if statement.submitted: messages.error(request, _("%(name)s is already submitted.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) if "apply" in request.POST: statement.submit(get_member(request)) messages.success(request, _("Successfully submited %(name)s. The finance department will notify the requestors as soon as possible.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) context = dict(self.admin_site.each_context(request), title=_('Submit statement'), opts=self.opts, statement=statement) return render(request, 'admin/submit_statement.html', context=context) class TransactionOnSubmittedStatementInline(admin.TabularInline): model = Transaction fields = ['amount', 'member', 'reference', 'ledger'] formfield_overrides = { TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} } extra = 0 class BillOnSubmittedStatementInline(BillOnStatementInline): model = BillOnStatementProxy extra = 0 sortable_options = [] fields = ['short_description', 'explanation', 'amount', 'paid_by', 'proof', 'costs_covered'] formfield_overrides = { TextField: {'widget': Textarea(attrs={'rows': 1, 'cols': 40})} } def get_readonly_fields(self, request, obj=None): return ['short_description', 'explanation', 'amount', 'paid_by', 'proof'] @admin.register(StatementSubmitted) class StatementSubmittedAdmin(admin.ModelAdmin): fields = ['short_description', 'explanation', 'excursion', 'submitted'] list_display = ['__str__', 'is_valid', 'submitted_date', 'submitted_by'] ordering = ('-submitted_date',) inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline] def has_add_permission(self, request, obj=None): return False def has_change_permission(self, request, obj=None): return True def get_readonly_fields(self, request, obj=None): readonly_fields = ['submitted'] if obj is not None and obj.submitted: return readonly_fields + self.fields else: return readonly_fields def get_urls(self): urls = super().get_urls() def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) wrapper.model_admin = self return update_wrapper(wrapper, view) custom_urls = [ path( "/overview/", wrap(self.overview_view), name="%s_%s_overview" % (self.opts.app_label, self.opts.model_name), ), path( "/reduce_transactions/", wrap(self.reduce_transactions_view), name="%s_%s_reduce_transactions" % (self.opts.app_label, self.opts.model_name), ), ] return custom_urls + urls def overview_view(self, request, object_id): statement = StatementSubmitted.objects.get(pk=object_id) if not statement.submitted: messages.error(request, _("%(name)s is not yet submitted.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,))) if "transaction_execution_confirm" in request.POST: res = statement.confirm(confirmer=get_member(request)) if not res: # this should NOT happen! messages.error(request, _("An error occured while trying to confirm %(name)s. Please try again.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name))) messages.success(request, _("Successfully confirmed %(name)s. I hope you executed the associated transactions, I wont remind you again.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) if "confirm" in request.POST: res = statement.validity if res == Statement.VALID: context = dict(self.admin_site.each_context(request), title=_('Statement confirmed'), opts=self.opts, statement=statement) return render(request, 'admin/confirmed_statement.html', context=context) elif res == Statement.NON_MATCHING_TRANSACTIONS: messages.error(request, _("Transactions do not match the covered expenses. Please correct the mistakes listed below.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,))) elif res == Statement.MISSING_LEDGER: messages.error(request, _("Some transactions have no ledger configured. Please fill in the gaps.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_overview' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,))) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) if "reject" in request.POST: statement.submitted = False statement.save() messages.success(request, _("Successfully rejected %(name)s. The requestor can reapply, when needed.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) if "generate_transactions" in request.POST: if statement.transaction_set.count() > 0: messages.error(request, _("%(name)s already has transactions. Please delete them first, if you want to generate new ones") % {'name': str(statement)}) else: success = statement.generate_transactions() if success: messages.success(request, _("Successfully generated transactions for %(name)s") % {'name': str(statement)}) else: messages.error(request, _("Error while generating transactions for %(name)s. Do all bills have a payer?") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,))) context = dict(self.admin_site.each_context(request), title=_('View submitted statement'), opts=self.opts, statement=statement, transaction_issues=statement.transaction_issues, **statement.template_context()) return render(request, 'admin/overview_submitted_statement.html', context=context) def reduce_transactions_view(self, request, object_id): statement = StatementSubmitted.objects.get(pk=object_id) statement.reduce_transactions() messages.success(request, _("Successfully reduced transactions for %(name)s.") % {'name': str(statement)}) return HttpResponseRedirect(request.GET['redirectTo']) #return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,))) @admin.register(StatementConfirmed) class StatementConfirmedAdmin(admin.ModelAdmin): fields = ['short_description', 'explanation', 'excursion', 'confirmed'] #readonly_fields = fields list_display = ['__str__', 'total_pretty', 'confirmed_date', 'confirmed_by'] ordering = ('-confirmed_date',) inlines = [BillOnSubmittedStatementInline, TransactionOnSubmittedStatementInline] def has_add_permission(self, request, obj=None): # To preserve integrity, no one is allowed to add confirmed statements return False def has_change_permission(self, request, obj=None): # To preserve integrity, no one is allowed to change confirmed statements return False def get_urls(self): urls = super().get_urls() def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) wrapper.model_admin = self return update_wrapper(wrapper, view) custom_urls = [ path( "/unconfirm/", wrap(self.unconfirm_view), name="%s_%s_unconfirm" % (self.opts.app_label, self.opts.model_name), ), ] return custom_urls + urls def unconfirm_view(self, request, object_id): statement = StatementConfirmed.objects.get(pk=object_id) if not statement.confirmed: messages.error(request, _("%(name)s is not yet confirmed.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_change' % (self.opts.app_label, self.opts.model_name), args=(statement.pk,))) if "unconfirm" in request.POST: statement.confirmed = False statement.confirmed_date = None statement.confired_by = None statement.save() messages.success(request, _("Successfully unconfirmed %(name)s. I hope you know what you are doing.") % {'name': str(statement)}) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (self.opts.app_label, self.opts.model_name))) context = dict(self.admin_site.each_context(request), title=_('Unconfirm statement'), opts=self.opts, statement=statement) return render(request, 'admin/unconfirm_statement.html', context=context) @admin.register(Transaction) class TransactionAdmin(admin.ModelAdmin): list_display = ['member', 'ledger', 'amount', 'reference', 'statement', 'confirmed', 'confirmed_date', 'confirmed_by'] list_filter = ('ledger', 'member', 'statement', 'confirmed') search_fields = ('reference', ) fields = ['reference', 'amount', 'member', 'ledger', 'statement'] def get_readonly_fields(self, request, obj=None): if obj is not None and obj.confirmed: return self.fields return super(TransactionAdmin, self).get_readonly_fields(request, obj) @admin.register(Bill) class BillAdmin(admin.ModelAdmin): list_display = ['__str__', 'statement', 'explanation', 'pretty_amount', 'paid_by', 'refunded'] list_filter = ('statement', 'paid_by', 'refunded') search_fields = ('reference', 'statement')