Hide keyboard shortcuts

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 

27 

28 

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) 

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 

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

66 

67 

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) 

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 

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

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

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

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

156 

157 

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 

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 

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 

332 

333 

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) 

360 

361 

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) 

368 

369 

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) 

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 

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 ] 

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 

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 ] 

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 

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) 

523 

524 

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

552def 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 

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

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 

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

705 qs.update(is_asset=True) 

706 

707 

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

709 qs.update(is_asset=False) 

710 

711 

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 

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 

733class ContractAdmin(ModelAdminBase): 

734 list_display = [ 

735 'id', 

736 'name', 

737 ] 

738 ordering = ['-id', ] 

739 allow_add = True 

740 allow_delete = True 

741 

742 

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) 

749 

750 

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) 

757 

758 

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 

784 

785 

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 

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 

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 

831 

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)