Coverage for jacc/admin.py : 48%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# pylint: disable=protected-access
2from datetime import datetime
3from decimal import Decimal
4from typing import Optional, List, Sequence, Any
5from django.contrib import messages
6from django.contrib.admin import SimpleListFilter
7from django.contrib.messages import add_message, INFO
8from django.db.models.functions import Coalesce
9from django import forms
10from django.urls import reverse, ResolverMatch
11from django.utils.formats import date_format
12from django.utils.html import format_html
13from django.utils.safestring import mark_safe
14from django.utils.text import format_lazy
15from jacc.models import Account, AccountEntry, Invoice, AccountType, EntryType, AccountEntrySourceFile, INVOICE_STATE
16from django.conf import settings
17from django.conf.urls import url
18from django.contrib import admin
19from django.contrib.auth.models import User
20from django.core.exceptions import ValidationError
21from django.db.models import QuerySet, Sum, Count
22from django.http import HttpRequest
23from django.utils.translation import gettext_lazy as _
24from jacc.settle import settle_assigned_invoice
25from jutil.admin import ModelAdminBase, admin_log
26from jutil.format import choices_label
29def align_lines(lines: list, column_separator: str = '|') -> list:
30 """
31 Pads lines so that all rows in single column match. Columns separated by '|' in every line.
32 :param lines: list of lines
33 :param column_separator: column separator. default is '|'
34 :return: list of lines
35 """
36 rows = []
37 col_len: List[int] = []
38 for line in lines:
39 line = str(line)
40 cols = []
41 for col_index, col in enumerate(line.split(column_separator)):
42 col = str(col).strip()
43 cols.append(col)
44 if col_index >= len(col_len):
45 col_len.append(0)
46 col_len[col_index] = max(col_len[col_index], len(col))
47 rows.append(cols)
49 lines_out: List[str] = []
50 for row in rows:
51 cols_out = []
52 for col_index, col in enumerate(row):
53 if col_index == 0:
54 col = col.ljust(col_len[col_index])
55 else:
56 col = col.rjust(col_len[col_index])
57 cols_out.append(col)
58 lines_out.append(' '.join(cols_out))
59 return lines_out
62def refresh_cached_fields(modeladmin, request, qs): # pylint: disable=unused-argument
63 for e in qs:
64 e.update_cached_fields()
65 add_message(request, messages.SUCCESS, 'Cached fields refreshed ({})'.format(qs.count()))
68def summarize_account_entries(modeladmin, request, qs): # pylint: disable=unused-argument
69 # {total_count} entries:
70 # {amount1} {currency} x {count1} = {total1} {currency}
71 # {amount2} {currency} x {count2} = {total2} {currency}
72 # Total {total_amount} {currency}
73 e_type_entries = list(qs.distinct('type').order_by('type'))
74 total_debits = Decimal('0.00')
75 total_credits = Decimal('0.00')
76 lines = ['<pre>',
77 _('({total_count} account entries)').format(total_count=qs.count())]
78 for e_type_entry in e_type_entries:
79 assert isinstance(e_type_entry, AccountEntry)
80 e_type = e_type_entry.type
81 assert isinstance(e_type, EntryType)
83 qs2 = qs.filter(type=e_type)
84 res_debit = qs2.filter(amount__gt=0).aggregate(total=Coalesce(Sum('amount'), 0), count=Count('amount'))
85 res_credit = qs2.filter(amount__lt=0).aggregate(total=Coalesce(Sum('amount'), 0), count=Count('amount'))
86 lines.append('{type_name} (debit) | x{count} | {total:.2f}'.format(type_name=e_type.name, **res_debit))
87 lines.append('{type_name} (credit) | x{count} | {total:.2f}'.format(type_name=e_type.name, **res_credit))
88 total_debits += res_debit['total']
89 total_credits += res_credit['total']
91 lines.append(_('Total debits {total_debits:.2f} | - total credits {total_credits:.2f} | = {total_amount:.2f}').format(
92 total_debits=total_debits, total_credits=total_credits, total_amount=total_debits + total_credits))
93 lines = align_lines(lines, '|')
94 messages.add_message(request, INFO, format_html('<br>'.join(lines)), extra_tags='safe')
97class SettlementAccountEntryFilter(SimpleListFilter):
98 title = _('settlement')
99 parameter_name = 'is_settlement'
101 def lookups(self, request, model_admin):
102 return [
103 ('1', _('settlement')),
104 ('0', _('not settlement')),
105 ]
107 def queryset(self, request, queryset):
108 val = self.value()
109 if val:
110 if val == '1':
111 queryset = queryset.filter(parent=None, type__is_settlement=True)
112 elif val == '0':
113 queryset = queryset.exclude(type__is_settlement=True)
114 return queryset
117class EntryTypeAccountEntryFilter(SimpleListFilter):
118 title = _('account entry type')
119 parameter_name = 'type'
121 def lookups(self, request, model_admin):
122 a = []
123 for e in EntryType.objects.all().filter(is_settlement=True).order_by('name'):
124 a.append((e.code, e.name))
125 return a
127 def queryset(self, request, queryset):
128 val = self.value()
129 if val:
130 queryset = queryset.filter(type__code=val)
131 return queryset
134class AccountTypeAccountEntryFilter(SimpleListFilter):
135 title = _('account type')
136 parameter_name = 'atype'
138 def lookups(self, request, model_admin):
139 a = []
140 for e in AccountType.objects.all().order_by('name'):
141 a.append((e.code, e.name))
142 return a
144 def queryset(self, request, queryset):
145 val = self.value()
146 if val:
147 queryset = queryset.filter(account__type__code=val)
148 return queryset
151class AccountEntryAdminForm(forms.ModelForm):
152 def clean(self):
153 if self.instance.archived:
154 raise ValidationError(_('cannot.modify.archived.account.entry'))
155 return super().clean()
158class AccountEntryAdmin(ModelAdminBase):
159 form = AccountEntryAdminForm
160 date_hierarchy = 'timestamp'
161 list_per_page = 50
162 actions = [
163 summarize_account_entries,
164 ]
165 list_display: Sequence[str] = [
166 'id',
167 'timestamp',
168 'type',
169 'amount',
170 'account_link',
171 'source_invoice_link',
172 'settled_invoice_link',
173 'settled_item_link',
174 'source_file_link',
175 'parent',
176 ]
177 raw_id_fields: Sequence[str] = [
178 'account',
179 'source_file',
180 'type',
181 'parent',
182 'source_invoice',
183 'settled_invoice',
184 'settled_item',
185 'parent',
186 ]
187 ordering: Sequence[str] = [
188 '-id',
189 ]
190 search_fields: Sequence[str] = [
191 'description',
192 '=amount',
193 ]
194 fields: Sequence[str] = [
195 'id',
196 'account',
197 'timestamp',
198 'created',
199 'last_modified',
200 'type',
201 'description',
202 'amount',
203 'source_file',
204 'source_invoice',
205 'settled_invoice',
206 'settled_item',
207 'parent',
208 'archived',
209 ]
210 readonly_fields: Sequence[str] = [
211 'id',
212 'created',
213 'last_modified',
214 'balance',
215 'source_invoice_link',
216 'settled_invoice_link',
217 'settled_item_link',
218 'archived',
219 ]
220 list_filter: Sequence[Any] = [
221 SettlementAccountEntryFilter,
222 EntryTypeAccountEntryFilter,
223 AccountTypeAccountEntryFilter,
224 'archived',
225 ]
226 account_admin_change_view_name = 'admin:jacc_account_change'
227 invoice_admin_change_view_name = 'admin:jacc_invoice_change'
228 accountentrysourcefile_admin_change_view_name = 'admin:jacc_accountentrysourcefile_change'
229 accountentry_admin_change_view_name = 'admin:jacc_accountentry_change'
230 allow_add = False
231 allow_delete = False
232 allow_change = False
234 def source_file_link(self, obj):
235 assert isinstance(obj, AccountEntry)
236 if not obj.source_file:
237 return ''
238 admin_url = reverse(self.accountentrysourcefile_admin_change_view_name, args=(obj.source_file.id, ))
239 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.source_file)
240 source_file_link.admin_order_field = 'source_file' # type: ignore
241 source_file_link.short_description = _('account entry source file') # type: ignore
243 def account_link(self, obj):
244 admin_url = reverse(self.account_admin_change_view_name, args=(obj.account.id, ))
245 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.account)
246 account_link.admin_order_field = 'account' # type: ignore
247 account_link.short_description = _('account') # type: ignore
249 def source_invoice_link(self, obj):
250 if not obj.source_invoice:
251 return ''
252 admin_url = reverse(self.invoice_admin_change_view_name, args=(obj.source_invoice.id, ))
253 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.source_invoice)
254 source_invoice_link.admin_order_field = 'source_invoice' # type: ignore
255 source_invoice_link.short_description = _('source invoice') # type: ignore
257 def settled_invoice_link(self, obj):
258 if not obj.settled_invoice:
259 return ''
260 admin_url = reverse(self.invoice_admin_change_view_name, args=(obj.settled_invoice.id, ))
261 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.settled_invoice)
262 settled_invoice_link.admin_order_field = 'settled_invoice' # type: ignore
263 settled_invoice_link.short_description = _('settled invoice') # type: ignore
265 def settled_item_link(self, obj):
266 if not obj.settled_item:
267 return ''
268 admin_url = reverse(self.accountentry_admin_change_view_name, args=(obj.settled_item.id, ))
269 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.settled_item)
270 settled_item_link.admin_order_field = 'settled_item' # type: ignore
271 settled_item_link.short_description = _('settled item') # type: ignore
273 def get_urls(self):
274 info = self.model._meta.app_label, self.model._meta.model_name
275 return [
276 url(r'^by-account/(?P<pk>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view), name='%s_%s_account_changelist' % info),
277 url(r'^by-source-invoice/(?P<pk>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view), name='%s_%s_source_invoice_changelist' % info),
278 url(r'^by-settled-invoice/(?P<pk>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view), name='%s_%s_settled_invoice_changelist' % info),
279 url(r'^by-source-file/(?P<pk>\d+)/$', self.admin_site.admin_view(self.kw_changelist_view), name='%s_%s_sourcefile_changelist' % info),
280 ] + super().get_urls()
282 def get_queryset(self, request: HttpRequest):
283 qs = super().get_queryset(request)
284 rm = request.resolver_match
285 assert isinstance(rm, ResolverMatch)
286 pk = rm.kwargs.get('pk', None)
287 if rm.url_name == 'jacc_accountentry_account_changelist' and pk:
288 return qs.filter(account=pk)
289 if rm.url_name == 'jacc_accountentry_sourcefile_invoice_changelist' and pk:
290 return qs.filter(source_invoice=pk)
291 if rm.url_name == 'jacc_accountentry_settled_invoice_changelist' and pk:
292 return qs.filter(settled_invoice=pk)
293 if rm.url_name == 'jacc_accountentry_sourcefile_changelist' and pk:
294 return qs.filter(source_file=pk)
295 return qs
298class AccountAdmin(ModelAdminBase):
299 list_display: Sequence[str] = [
300 'id',
301 'type',
302 'name',
303 'balance',
304 'currency',
305 'is_asset',
306 ]
307 fields: Sequence[str] = [
308 'id',
309 'type',
310 'name',
311 'balance',
312 'currency',
313 ]
314 readonly_fields: Sequence[str] = [
315 'id',
316 'balance',
317 'is_asset',
318 ]
319 raw_id_fields: Sequence[str] = [
320 'type',
321 ]
322 ordering: Sequence[str] = [
323 '-id',
324 ]
325 list_filter: Sequence[Any] = [
326 'type',
327 'type__is_asset',
328 ]
329 allow_add = True
330 allow_delete = True
331 list_per_page = 20
334class AccountEntryInlineFormSet(forms.BaseInlineFormSet):
335 def clean_entries(self, source_invoice: Optional[Invoice], settled_invoice: Optional[Invoice], account: Optional[Account], **kw):
336 """
337 This needs to be called from a derived class clean().
338 :param source_invoice:
339 :param settled_invoice:
340 :param account:
341 :return: None
342 """
343 for form in self.forms:
344 obj = form.instance
345 assert isinstance(obj, AccountEntry)
346 if account is not None:
347 obj.account = account
348 obj.source_invoice = source_invoice
349 obj.settled_invoice = settled_invoice
350 if obj.parent:
351 if obj.amount is None:
352 obj.amount = obj.parent.amount
353 if obj.type is None:
354 obj.type = obj.parent.type
355 if obj.amount is not None and obj.parent.amount is not None:
356 if obj.amount > obj.parent.amount > Decimal(0) or obj.amount < obj.parent.amount < Decimal(0):
357 raise ValidationError(_('Derived account entry amount cannot be larger than original'))
358 for k, v in kw.items():
359 setattr(obj, k, v)
362class SingleReceivablesAccountInvoiceItemInlineFormSet(AccountEntryInlineFormSet):
363 def clean(self):
364 instance = self.instance
365 assert isinstance(instance, Invoice)
366 receivables_account = Account.objects.get(type__code=settings.ACCOUNT_RECEIVABLES)
367 self.clean_entries(instance, None, receivables_account)
370class SingleSettlementsAccountSettlementInlineFormSet(AccountEntryInlineFormSet):
371 def clean(self):
372 instance = self.instance
373 assert isinstance(instance, Invoice)
374 settlement_account = Account.objects.get(type__code=settings.ACCOUNT_SETTLEMENTS)
375 self.clean_entries(None, instance, settlement_account)
377 def save(self, commit=True):
378 instance = self.instance
379 assert isinstance(instance, Invoice)
380 entries = super().save(commit)
381 settlement_account = Account.objects.get(type__code=settings.ACCOUNT_SETTLEMENTS)
382 assert isinstance(settlement_account, Account)
383 for e in entries:
384 if settlement_account.needs_settling(e):
385 settle_assigned_invoice(instance.receivables_account, e, AccountEntry)
386 return entries
389class InvoiceItemInline(admin.TabularInline): # TODO: override in app
390 model = AccountEntry
391 formset = SingleReceivablesAccountInvoiceItemInlineFormSet # TODO: override in app
392 fk_name = 'source_invoice'
393 verbose_name = _('invoice items')
394 verbose_name_plural = _('invoices items')
395 extra = 0
396 can_delete = True
397 account_entry_change_view_name = 'admin:jacc_accountentry_change'
398 fields = [
399 'id_link',
400 'timestamp',
401 'type',
402 'description',
403 'amount',
404 ]
405 raw_id_fields = [
406 'account',
407 'type',
408 'source_invoice',
409 'settled_invoice',
410 'settled_item',
411 'source_file',
412 'parent',
413 ]
414 readonly_fields = [
415 'id_link',
416 ]
418 def id_link(self, obj):
419 if obj and obj.id:
420 assert isinstance(obj, AccountEntry)
421 admin_url = reverse(self.account_entry_change_view_name, args=(obj.id, ))
422 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.id)
423 return ''
424 id_link.admin_order_field = 'id' # type: ignore
425 id_link.short_description = _('id') # type: ignore
427 def get_queryset(self, request):
428 queryset = self.model._default_manager.get_queryset().filter(type__is_settlement=False)
429 if not self.has_change_permission(request):
430 queryset = queryset.none()
431 return queryset
433 def get_field_queryset(self, db, db_field, request):
434 related_admin = self.admin_site._registry.get(db_field.remote_field.model)
435 if related_admin and db_field.name == 'type':
436 return related_admin.get_queryset(request).filter(is_settlement=False).order_by('name')
437 return super().get_field_queryset(db, db_field, request)
440class InvoiceSettlementInline(admin.TabularInline): # TODO: override in app
441 model = AccountEntry
442 formset = SingleSettlementsAccountSettlementInlineFormSet # TODO: override in app
443 fk_name = 'settled_invoice'
444 verbose_name = _('settlements')
445 verbose_name_plural = _('settlements')
446 show_non_settlements = False
447 extra = 0
448 can_delete = True
449 account_entry_change_view_name = 'admin:jacc_accountentry_change' # TODO: override in app
450 account_change_view_name = 'admin:jacc_account_change' # TODO: override in app
451 fields = [
452 'id_link',
453 'account_link',
454 'timestamp',
455 'type',
456 'description',
457 'amount',
458 'parent',
459 'settled_item',
460 ]
461 raw_id_fields = [
462 'account',
463 'type',
464 'source_invoice',
465 'settled_invoice',
466 'source_file',
467 'parent',
468 'settled_item',
469 ]
470 readonly_fields = [
471 'id_link',
472 'account_link',
473 'settled_item',
474 ]
476 def get_queryset(self, request):
477 queryset = self.model._default_manager.get_queryset()
478 if not self.show_non_settlements:
479 queryset = queryset.filter(type__is_settlement=True)
480 if not self.has_change_permission(request):
481 queryset = queryset.none()
482 return queryset
484 def get_field_queryset(self, db, db_field, request):
485 related_admin = self.admin_site._registry.get(db_field.remote_field.model)
486 if related_admin and db_field.name == 'type' and not self.show_non_settlements:
487 return related_admin.get_queryset(request).filter(is_settlement=True).order_by('name')
488 return super().get_field_queryset(db, db_field, request)
490 def id_link(self, obj):
491 if obj and obj.id:
492 assert isinstance(obj, AccountEntry)
493 admin_url = reverse(self.account_entry_change_view_name, args=(obj.id, ))
494 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.id)
495 return ''
496 id_link.admin_order_field = 'id' # type: ignore
497 id_link.short_description = _('id') # type: ignore
499 def account_link(self, obj):
500 if obj and obj.id:
501 assert isinstance(obj, AccountEntry)
502 admin_url = reverse(self.account_change_view_name, args=(obj.account.id, ))
503 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.account)
504 return ''
505 account_link.admin_order_field = 'account' # type: ignore
506 account_link.short_description = _('account') # type: ignore
509def resend_invoices(modeladmin, request: HttpRequest, queryset: QuerySet): # pylint: disable=unused-argument
510 """
511 Marks invoices with as un-sent.
512 :param modeladmin:
513 :param request:
514 :param queryset:
515 :return:
516 """
517 user = request.user
518 assert isinstance(user, User)
519 for obj in queryset:
520 assert isinstance(obj, Invoice)
521 admin_log([obj, user], 'Invoice id={invoice} marked for re-sending'.format(invoice=obj.id), who=user)
522 queryset.update(sent=None)
525class InvoiceLateDaysFilter(SimpleListFilter):
526 title = _('late days')
527 parameter_name = 'late_days_range'
529 def lookups(self, request, model_admin):
530 if hasattr(settings, 'INVOICE_LATE_DAYS_LIST_FILTER'):
531 return settings.INVOICE_LATE_DAYS_LIST_FILTER
532 return [
533 ('<0', _('late.days.filter.not.due')),
534 ('0<7', format_lazy(_('late.days.filter.late.range'), 1, 7)),
535 ('7<14', format_lazy(_('late.days.filter.late.range'), 7, 14)),
536 ('14<21', format_lazy(_('late.days.filter.late.range'), 14, 21)),
537 ('21<28', format_lazy(_('late.days.filter.late.range'), 21, 28)),
538 ('28<', format_lazy(_('late.days.filter.late.over.days'), 28)),
539 ]
541 def queryset(self, request, queryset):
542 val = self.value()
543 if val:
544 begin, end = str(val).split('<')
545 if begin:
546 queryset = queryset.filter(late_days__gte=int(begin))
547 if end:
548 queryset = queryset.filter(late_days__lt=int(end))
549 return queryset
552def summarize_invoice_statistics(modeladmin, request: HttpRequest, qs: QuerySet): # pylint: disable=unused-argument
553 invoice_states = list([state for state, name in INVOICE_STATE])
555 invoiced_total_amount = Decimal('0.00')
556 invoiced_total_count = 0
558 lines = [
559 '<pre>',
560 _('({total_count} invoices)').format(total_count=qs.count()),
561 ]
562 for state in invoice_states:
563 state_name = choices_label(INVOICE_STATE, state)
564 qs2 = qs.filter(state=state)
566 invoiced = qs2.filter(state=state).aggregate(amount=Coalesce(Sum('amount'), 0), count=Count('*'))
567 invoiced_amount = Decimal(invoiced['amount'])
568 invoiced_count = int(invoiced['count'])
569 invoiced_total_amount += invoiced_amount
570 invoiced_total_count += invoiced_count
572 lines.append('{state_name} | x{count} | {amount:.2f}'.format(
573 state_name=state_name, amount=invoiced_amount, count=invoiced_count))
575 lines.append(_('Total') + ' {label} | x{count} | {amount:.2f}'.format(
576 label=_('amount'), amount=invoiced_total_amount, count=invoiced_total_count))
577 lines.append('</pre>')
579 lines = align_lines(lines, '|')
580 messages.add_message(request, INFO, format_html('<br>'.join(lines)), extra_tags='safe')
583class InvoiceAdmin(ModelAdminBase):
584 """
585 Invoice admin. Override following in derived classes:
586 - InvoiceSettlementInline with formset derived from AccountEntryInlineFormSet, override clean and call clean_entries()
587 - InvoiceItemsInline with formset derived from AccountEntryInlineFormSet, override clean and call clean_entries()
588 - inlines = [] set with above mentioned derived classes
589 """
590 date_hierarchy = 'created'
591 actions = [
592 summarize_invoice_statistics,
593 refresh_cached_fields,
594 # resend_invoices,
595 ]
596 # override in derived class
597 inlines = [
598 InvoiceItemInline, # TODO: override in app
599 InvoiceSettlementInline, # TODO: override in app
600 ]
601 list_display: Sequence[str] = [
602 'number',
603 'created_brief',
604 'sent_brief',
605 'due_date_brief',
606 'close_date_brief',
607 'late_days',
608 'amount',
609 'paid_amount',
610 'unpaid_amount',
611 ]
612 fields: Sequence[str] = [
613 'type',
614 'number',
615 'due_date',
616 'notes',
617 'filename',
618 'amount',
619 'paid_amount',
620 'unpaid_amount',
621 'state',
622 'overpaid_amount',
623 'close_date',
624 'late_days',
625 'created',
626 'last_modified',
627 'sent',
628 ]
629 readonly_fields: Sequence[str] = [
630 'created',
631 'last_modified',
632 'sent',
633 'close_date',
634 'created_brief',
635 'sent_brief',
636 'due_date_brief',
637 'close_date_brief',
638 'filename',
639 'amount',
640 'paid_amount',
641 'unpaid_amount',
642 'state',
643 'overpaid_amount',
644 'late_days',
645 ]
646 raw_id_fields: Sequence[str] = [
647 ]
648 search_fields: Sequence[str] = [
649 '=amount',
650 '=filename',
651 '=number',
652 ]
653 list_filter: Sequence[Any] = [
654 'state',
655 InvoiceLateDaysFilter,
656 ]
657 allow_add = True
658 allow_delete = True
659 ordering = ('-id', )
661 def construct_change_message(self, request, form, formsets, add=False):
662 instance = form.instance
663 assert isinstance(instance, Invoice)
664 instance.update_cached_fields()
665 return super().construct_change_message(request, form, formsets, add)
667 def _format_date(self, obj) -> str:
668 """
669 Short date format.
670 :param obj: date or datetime or None
671 :return: str
672 """
673 if obj is None:
674 return ''
675 if isinstance(obj, datetime):
676 obj = obj.date()
677 return date_format(obj, 'SHORT_DATE_FORMAT')
679 def created_brief(self, obj):
680 assert isinstance(obj, Invoice)
681 return self._format_date(obj.created)
682 created_brief.admin_order_field = 'created' # type: ignore
683 created_brief.short_description = _('created') # type: ignore
685 def sent_brief(self, obj):
686 assert isinstance(obj, Invoice)
687 return self._format_date(obj.sent)
688 sent_brief.admin_order_field = 'sent' # type: ignore
689 sent_brief.short_description = _('sent') # type: ignore
691 def due_date_brief(self, obj):
692 assert isinstance(obj, Invoice)
693 return self._format_date(obj.due_date)
694 due_date_brief.admin_order_field = 'due_date' # type: ignore
695 due_date_brief.short_description = _('due date') # type: ignore
697 def close_date_brief(self, obj):
698 assert isinstance(obj, Invoice)
699 return self._format_date(obj.close_date)
700 close_date_brief.admin_order_field = 'close_date' # type: ignore
701 close_date_brief.short_description = _('close date') # type: ignore
704def set_as_asset(modeladmin, request, qs): # pylint: disable=unused-argument
705 qs.update(is_asset=True)
708def set_as_liability(modeladmin, request, qs): # pylint: disable=unused-argument
709 qs.update(is_asset=False)
712class AccountTypeAdmin(ModelAdminBase):
713 list_display = [
714 'code',
715 'name',
716 'is_asset',
717 'is_liability',
718 ]
719 actions = [
720 set_as_asset,
721 set_as_liability,
722 ]
723 ordering = ('name', )
724 allow_add = True
725 allow_delete = True
727 def is_liability(self, obj):
728 return obj.is_liability
729 is_liability.short_description = _('is liability') # type: ignore
730 is_liability.boolean = True # type: ignore
733class ContractAdmin(ModelAdminBase):
734 list_display = [
735 'id',
736 'name',
737 ]
738 ordering = ['-id', ]
739 allow_add = True
740 allow_delete = True
743def toggle_settlement(modeladmin, request: HttpRequest, queryset: QuerySet): # pylint: disable=unused-argument
744 for e in queryset:
745 assert isinstance(e, EntryType)
746 e.is_settlement = not e.is_settlement
747 e.save()
748 admin_log([e], 'Toggled settlement flag {}'.format('on' if e.is_settlement else 'off'), who=request.user)
751def toggle_payment(modeladmin, request: HttpRequest, queryset: QuerySet): # pylint: disable=unused-argument
752 for e in queryset:
753 assert isinstance(e, EntryType)
754 e.is_payment = not e.is_payment
755 e.save()
756 admin_log([e], 'Toggled payment flag {}'.format('on' if e.is_settlement else 'off'), who=request.user)
759class EntryTypeAdmin(ModelAdminBase):
760 list_display = [
761 'id',
762 'identifier',
763 'name',
764 'is_settlement',
765 'is_payment',
766 'payback_priority',
767 ]
768 list_filter: Sequence[Any] = (
769 'is_settlement',
770 'is_payment',
771 )
772 search_fields: Sequence[str] = (
773 'name',
774 'code',
775 )
776 actions = [
777 toggle_settlement,
778 toggle_payment,
779 ]
780 exclude: Sequence[str] = ()
781 ordering: Sequence[str] = ['name', ]
782 allow_add = True
783 allow_delete = True
786class AccountEntrySourceFileAdmin(ModelAdminBase):
787 list_display: Sequence[str] = [
788 'id',
789 'created',
790 'entries_link',
791 ]
792 date_hierarchy = 'created'
793 ordering: Sequence[str] = [
794 '-id',
795 ]
796 fields: Sequence[str] = [
797 'id',
798 'name',
799 'created',
800 'last_modified',
801 ]
802 search_fields: Sequence[str] = [
803 '=name',
804 ]
805 readonly_fields: Sequence[str] = [
806 'id',
807 'created',
808 'name',
809 'last_modified',
810 'entries_link',
811 ]
812 allow_add = True
813 allow_delete = True
815 def entries_link(self, obj):
816 if obj and obj.id:
817 assert isinstance(obj, AccountEntrySourceFile)
818 admin_url = reverse('admin:jacc_accountentry_sourcefile_changelist', args=(obj.id, ))
819 return format_html("<a href='{}'>{}</a>", mark_safe(admin_url), obj.name)
820 return ''
821 entries_link.admin_order_field = 'name' # type: ignore
822 entries_link.short_description = _('account entry source file') # type: ignore
825resend_invoices.short_description = _('Re-send invoices') # type: ignore
826refresh_cached_fields.short_description = _('Refresh cached fields') # type: ignore
827summarize_account_entries.short_description = _('Summmarize account entries') # type: ignore
828summarize_invoice_statistics.short_description = _('Summarize invoice statistics') # type: ignore
829set_as_asset.short_description = _('set_as_asset') # type: ignore
830set_as_liability.short_description = _('set_as_liability') # type: ignore
832admin.site.register(Account, AccountAdmin)
833admin.site.register(Invoice, InvoiceAdmin) # TODO: override in app
834admin.site.register(AccountEntry, AccountEntryAdmin) # TODO: override in app
835admin.site.register(AccountType, AccountTypeAdmin)
836admin.site.register(EntryType, EntryTypeAdmin)
837admin.site.register(AccountEntrySourceFile, AccountEntrySourceFileAdmin)