Coverage for jacc/admin.py: 48%

# Coverage for jacc/admin.py : 48%

## 462 statements 224 run 238 missing 0 excluded

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

2 from datetime import datetime

3 from decimal import Decimal

4 from typing import Optional, List, Sequence, Any

5 from django.contrib import messages

6 from django.contrib.admin import SimpleListFilter

7 from django.contrib.messages import add_message, INFO

8 from django.db.models.functions import Coalesce

9 from django import forms

10 from django.urls import reverse, ResolverMatch

11 from django.utils.formats import date_format

12 from django.utils.html import format_html

13 from django.utils.safestring import mark_safe

14 from django.utils.text import format_lazy

15 from jacc.models import Account, AccountEntry, Invoice, AccountType, EntryType, AccountEntrySourceFile, INVOICE_STATE

16 from django.conf import settings

17 from django.conf.urls import url

18 from django.contrib import admin

19 from django.contrib.auth.models import User

20 from django.core.exceptions import ValidationError

21 from django.db.models import QuerySet, Sum, Count

22 from django.http import HttpRequest

23 from django.utils.translation import gettext_lazy as _

24 from jacc.settle import settle_assigned_invoice

25 from jutil.admin import ModelAdminBase, admin_log

26 from jutil.format import choices_label

27

28

29 def 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)

48

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

60

61

62 def 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()))

66

67

68 def 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)

82

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']

90

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')

95

96

97 class SettlementAccountEntryFilter(SimpleListFilter):

98 title = _('settlement')

99 parameter_name = 'is_settlement'

100

101 def lookups(self, request, model_admin):

102 return [

103 ('1', _('settlement')),

104 ('0', _('not settlement')),

105 ]

106

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

115

116

117 class EntryTypeAccountEntryFilter(SimpleListFilter):

118 title = _('account entry type')

119 parameter_name = 'type'

120

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

126

127 def queryset(self, request, queryset):

128 val = self.value()

129 if val:

130 queryset = queryset.filter(type__code=val)

131 return queryset

132

133

134 class AccountTypeAccountEntryFilter(SimpleListFilter):

135 title = _('account type')

136 parameter_name = 'atype'

137

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

143

144 def queryset(self, request, queryset):

145 val = self.value()

146 if val:

147 queryset = queryset.filter(account__type__code=val)

148 return queryset

149

150

151 class AccountEntryAdminForm(forms.ModelForm):

152 def clean(self):

153 if self.instance.archived:

154 raise ValidationError(_('cannot.modify.archived.account.entry'))

155 return super().clean()

156

157

158 class 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

233

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

242

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

248

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

256

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

264

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

272

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()

281

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

296

297

298 class 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

332

333

334 class 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)

360

361

362 class 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)

368

369

370 class 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)

376

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

387

388

389 class 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 ]

417

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

426

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

432

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)

438

439

440 class 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 ]

475

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

483

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)

489

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

498

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

507

508

509 def 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)

523

524

525 class InvoiceLateDaysFilter(SimpleListFilter):

526 title = _('late days')

527 parameter_name = 'late_days_range'

528

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 ]

540

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

550

551

552 def summarize_invoice_statistics(modeladmin, request: HttpRequest, qs: QuerySet): # pylint: disable=unused-argument

553 invoice_states = list([state for state, name in INVOICE_STATE])

554

555 invoiced_total_amount = Decimal('0.00')

556 invoiced_total_count = 0

557

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)

565

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

571

572 lines.append('{state_name} | x{count} | {amount:.2f}'.format(

573 state_name=state_name, amount=invoiced_amount, count=invoiced_count))

574

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>')

578

579 lines = align_lines(lines, '|')

580 messages.add_message(request, INFO, format_html('<br>'.join(lines)), extra_tags='safe')

581

582

583 class 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', )

660

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)

666

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')

678

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

684

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

690

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

696

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

702

703

704 def set_as_asset(modeladmin, request, qs): # pylint: disable=unused-argument

705 qs.update(is_asset=True)

706

707

708 def set_as_liability(modeladmin, request, qs): # pylint: disable=unused-argument

709 qs.update(is_asset=False)

710

711

712 class 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

726

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

731

732

733 class ContractAdmin(ModelAdminBase):

734 list_display = [

735 'id',

736 'name',

737 ]

738 ordering = ['-id', ]

739 allow_add = True

740 allow_delete = True

741

742

743 def 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)

749

750

751 def 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)

757

758

759 class 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

784

785

786 class 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

814

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

823

824

825 resend_invoices.short_description = _('Re-send invoices') # type: ignore

826 refresh_cached_fields.short_description = _('Refresh cached fields') # type: ignore

827 summarize_account_entries.short_description = _('Summmarize account entries') # type: ignore

828 summarize_invoice_statistics.short_description = _('Summarize invoice statistics') # type: ignore

829 set_as_asset.short_description = _('set_as_asset') # type: ignore

830 set_as_liability.short_description = _('set_as_liability') # type: ignore

831

832 admin.site.register(Account, AccountAdmin)

833 admin.site.register(Invoice, InvoiceAdmin) # TODO: override in app

834 admin.site.register(AccountEntry, AccountEntryAdmin) # TODO: override in app

835 admin.site.register(AccountType, AccountTypeAdmin)

836 admin.site.register(EntryType, EntryTypeAdmin)

837 admin.site.register(AccountEntrySourceFile, AccountEntrySourceFileAdmin)