django-ledger 0.8.0__py3-none-any.whl → 0.8.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-ledger might be problematic. Click here for more details.

Files changed (56) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/forms/account.py +45 -46
  3. django_ledger/forms/data_import.py +182 -64
  4. django_ledger/io/io_core.py +507 -374
  5. django_ledger/migrations/0026_stagedtransactionmodel_customer_model_and_more.py +56 -0
  6. django_ledger/models/__init__.py +2 -1
  7. django_ledger/models/bill.py +337 -300
  8. django_ledger/models/customer.py +47 -34
  9. django_ledger/models/data_import.py +770 -289
  10. django_ledger/models/entity.py +882 -637
  11. django_ledger/models/mixins.py +421 -282
  12. django_ledger/models/receipt.py +1083 -0
  13. django_ledger/models/transactions.py +105 -41
  14. django_ledger/models/unit.py +42 -30
  15. django_ledger/models/utils.py +12 -2
  16. django_ledger/models/vendor.py +85 -66
  17. django_ledger/settings.py +1 -0
  18. django_ledger/static/django_ledger/bundle/djetler.bundle.js +1 -1
  19. django_ledger/static/django_ledger/bundle/djetler.bundle.js.LICENSE.txt +1 -13
  20. django_ledger/templates/django_ledger/bills/bill_update.html +1 -1
  21. django_ledger/templates/django_ledger/components/period_navigator.html +5 -3
  22. django_ledger/templates/django_ledger/customer/customer_detail.html +87 -0
  23. django_ledger/templates/django_ledger/customer/customer_list.html +0 -1
  24. django_ledger/templates/django_ledger/customer/tags/customer_table.html +3 -1
  25. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_imported.html +24 -3
  26. django_ledger/templates/django_ledger/data_import/tags/data_import_job_txs_table.html +26 -10
  27. django_ledger/templates/django_ledger/entity/entity_dashboard.html +2 -2
  28. django_ledger/templates/django_ledger/invoice/invoice_update.html +1 -1
  29. django_ledger/templates/django_ledger/layouts/base.html +3 -1
  30. django_ledger/templates/django_ledger/layouts/content_layout_1.html +1 -1
  31. django_ledger/templates/django_ledger/receipt/customer_receipt_report.html +115 -0
  32. django_ledger/templates/django_ledger/receipt/receipt_delete.html +30 -0
  33. django_ledger/templates/django_ledger/receipt/receipt_detail.html +89 -0
  34. django_ledger/templates/django_ledger/receipt/receipt_list.html +134 -0
  35. django_ledger/templates/django_ledger/receipt/vendor_receipt_report.html +115 -0
  36. django_ledger/templates/django_ledger/vendor/tags/vendor_table.html +3 -2
  37. django_ledger/templates/django_ledger/vendor/vendor_detail.html +86 -0
  38. django_ledger/templatetags/django_ledger.py +338 -191
  39. django_ledger/urls/__init__.py +1 -0
  40. django_ledger/urls/customer.py +3 -0
  41. django_ledger/urls/data_import.py +3 -0
  42. django_ledger/urls/receipt.py +102 -0
  43. django_ledger/urls/vendor.py +1 -0
  44. django_ledger/views/__init__.py +1 -0
  45. django_ledger/views/customer.py +56 -14
  46. django_ledger/views/data_import.py +119 -66
  47. django_ledger/views/mixins.py +112 -86
  48. django_ledger/views/receipt.py +294 -0
  49. django_ledger/views/vendor.py +53 -14
  50. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/METADATA +1 -1
  51. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/RECORD +55 -45
  52. django_ledger/static/django_ledger/bundle/styles.bundle.js +0 -1
  53. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/WHEEL +0 -0
  54. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/licenses/AUTHORS.md +0 -0
  55. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/licenses/LICENSE +0 -0
  56. {django_ledger-0.8.0.dist-info → django_ledger-0.8.2.dist-info}/top_level.txt +0 -0
@@ -11,9 +11,13 @@ from datetime import timedelta, date
11
11
  from typing import Tuple, Optional
12
12
 
13
13
  from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
14
- from django.core.exceptions import ValidationError, ObjectDoesNotExist, ImproperlyConfigured
14
+ from django.core.exceptions import (
15
+ ValidationError,
16
+ ObjectDoesNotExist,
17
+ ImproperlyConfigured,
18
+ )
15
19
  from django.db.models import Q
16
- from django.http import Http404, HttpResponse, HttpResponseNotFound
20
+ from django.http import Http404, HttpResponse
17
21
  from django.urls import reverse
18
22
  from django.utils.dateparse import parse_date
19
23
  from django.utils.translation import gettext_lazy as _
@@ -35,15 +39,18 @@ class ContextFromToDateMixin:
35
39
  return self.TO_DATE_CONTEXT_NAME
36
40
 
37
41
 
38
- class YearlyReportMixIn(YearMixin, ContextFromToDateMixin, EntityModelFiscalPeriodMixIn):
39
-
42
+ class YearlyReportMixIn(
43
+ YearMixin, ContextFromToDateMixin, EntityModelFiscalPeriodMixIn
44
+ ):
40
45
  def get_from_date(self, year: int = None, fy_start: int = None, **kwargs) -> date:
41
46
  return self.get_year_start_date(year, fy_start)
42
47
 
43
48
  def get_to_date(self, year: int = None, fy_start: int = None, **kwargs) -> date:
44
49
  return self.get_year_end_date(year, fy_start)
45
50
 
46
- def get_from_to_dates(self, year: int = None, fy_start: int = None, **kwargs) -> Tuple[date, date]:
51
+ def get_from_to_dates(
52
+ self, year: int = None, fy_start: int = None, **kwargs
53
+ ) -> Tuple[date, date]:
47
54
  from_date = self.get_from_date(year, fy_start, **kwargs)
48
55
  to_date = self.get_to_date(year, fy_start, **kwargs)
49
56
  return from_date, to_date
@@ -78,7 +85,9 @@ class YearlyReportMixIn(YearMixin, ContextFromToDateMixin, EntityModelFiscalPeri
78
85
  return context
79
86
 
80
87
 
81
- class QuarterlyReportMixIn(YearMixin, ContextFromToDateMixin, EntityModelFiscalPeriodMixIn):
88
+ class QuarterlyReportMixIn(
89
+ YearMixin, ContextFromToDateMixin, EntityModelFiscalPeriodMixIn
90
+ ):
82
91
  quarter = None
83
92
  quarter_url_kwarg = 'quarter'
84
93
 
@@ -89,9 +98,11 @@ class QuarterlyReportMixIn(YearMixin, ContextFromToDateMixin, EntityModelFiscalP
89
98
  try:
90
99
  self.validate_quarter(quarter)
91
100
  except ValidationError:
92
- raise Http404(_("Invalid quarter number"))
101
+ raise Http404(_('Invalid quarter number'))
93
102
  except ValueError:
94
- raise Http404(_(f"Invalid quarter format. Cannot parse {quarter} into integer."))
103
+ raise Http404(
104
+ _(f'Invalid quarter format. Cannot parse {quarter} into integer.')
105
+ )
95
106
  return quarter
96
107
 
97
108
  def get_quarter(self) -> int:
@@ -103,33 +114,43 @@ class QuarterlyReportMixIn(YearMixin, ContextFromToDateMixin, EntityModelFiscalP
103
114
  try:
104
115
  quarter = self.request.GET[self.quarter_url_kwarg]
105
116
  except KeyError:
106
- raise Http404(_("No quarter specified"))
117
+ raise Http404(_('No quarter specified'))
107
118
  quarter = self.parse_quarter(quarter)
108
119
  return quarter
109
120
 
110
- def get_from_date(self, quarter: int = None, year: int = None, fy_start: int = None, **kwargs) -> date:
121
+ def get_from_date(
122
+ self, quarter: int = None, year: int = None, fy_start: int = None, **kwargs
123
+ ) -> date:
111
124
  return self.get_quarter_start_date(quarter, year, fy_start)
112
125
 
113
- def get_to_date(self, quarter: int = None, year: int = None, fy_start: int = None, **kwargs) -> date:
126
+ def get_to_date(
127
+ self, quarter: int = None, year: int = None, fy_start: int = None, **kwargs
128
+ ) -> date:
114
129
  return self.get_quarter_end_date(quarter, year, fy_start)
115
130
 
116
- def get_from_to_dates(self,
117
- quarter: int = None,
118
- year: int = None,
119
- fy_start: int = None,
120
- **kwargs) -> Tuple[date, date]:
121
- from_date = self.get_from_date(quarter=quarter, year=year, fy_start=fy_start, **kwargs)
122
- to_date = self.get_to_date(quarter=quarter, year=year, fy_start=fy_start, **kwargs)
131
+ def get_from_to_dates(
132
+ self, quarter: int = None, year: int = None, fy_start: int = None, **kwargs
133
+ ) -> Tuple[date, date]:
134
+ from_date = self.get_from_date(
135
+ quarter=quarter, year=year, fy_start=fy_start, **kwargs
136
+ )
137
+ to_date = self.get_to_date(
138
+ quarter=quarter, year=year, fy_start=fy_start, **kwargs
139
+ )
123
140
  return from_date, to_date
124
141
 
125
- def get_quarter_start_date(self, quarter: int = None, year: int = None, fy_start: int = None) -> date:
142
+ def get_quarter_start_date(
143
+ self, quarter: int = None, year: int = None, fy_start: int = None
144
+ ) -> date:
126
145
  if not year:
127
146
  year = self.get_year()
128
147
  if not quarter:
129
148
  quarter = self.get_quarter()
130
149
  return self.get_quarter_start(year, quarter, fy_start)
131
150
 
132
- def get_quarter_end_date(self, quarter: int = None, year: int = None, fy_start: int = None) -> date:
151
+ def get_quarter_end_date(
152
+ self, quarter: int = None, year: int = None, fy_start: int = None
153
+ ) -> date:
133
154
  if not year:
134
155
  year = self.get_year()
135
156
  if not quarter:
@@ -162,17 +183,15 @@ class QuarterlyReportMixIn(YearMixin, ContextFromToDateMixin, EntityModelFiscalP
162
183
 
163
184
 
164
185
  class MonthlyReportMixIn(YearlyReportMixIn, ContextFromToDateMixin, MonthMixin):
165
-
166
186
  def get_from_date(self, month: int = None, year: int = None, **kwargs) -> date:
167
187
  return self.get_month_start_date(month=month, year=year)
168
188
 
169
189
  def get_to_date(self, month: int = None, year: int = None, **kwargs) -> date:
170
190
  return self.get_month_end_date(month=month, year=year)
171
191
 
172
- def get_from_to_dates(self,
173
- month: int = None,
174
- year: int = None,
175
- **kwargs) -> Tuple[date, date]:
192
+ def get_from_to_dates(
193
+ self, month: int = None, year: int = None, **kwargs
194
+ ) -> Tuple[date, date]:
176
195
  from_date = self.get_from_date(month=month, year=year, **kwargs)
177
196
  to_date = self.get_to_date(month=month, year=year, **kwargs)
178
197
  return from_date, to_date
@@ -220,7 +239,6 @@ class MonthlyReportMixIn(YearlyReportMixIn, ContextFromToDateMixin, MonthMixin):
220
239
 
221
240
 
222
241
  class DateReportMixIn(MonthlyReportMixIn, ContextFromToDateMixin, DayMixin):
223
-
224
242
  def get_context_data(self, **kwargs):
225
243
  context = super(MonthlyReportMixIn, self).get_context_data(**kwargs)
226
244
  view_date = self.get_date()
@@ -233,11 +251,7 @@ class DateReportMixIn(MonthlyReportMixIn, ContextFromToDateMixin, DayMixin):
233
251
  return context
234
252
 
235
253
  def get_date(self) -> date:
236
- return date(
237
- year=self.get_year(),
238
- month=self.get_month(),
239
- day=self.get_day()
240
- )
254
+ return date(year=self.get_year(), month=self.get_month(), day=self.get_day())
241
255
 
242
256
  def get_from_date(self, month: int = None, year: int = None, **kwargs) -> date:
243
257
  return self.get_date()
@@ -245,7 +259,9 @@ class DateReportMixIn(MonthlyReportMixIn, ContextFromToDateMixin, DayMixin):
245
259
  def get_to_date(self, month: int = None, year: int = None, **kwargs) -> date:
246
260
  return self.get_date()
247
261
 
248
- def get_from_to_dates(self, month: int = None, year: int = None, **kwargs) -> Tuple[date, date]:
262
+ def get_from_to_dates(
263
+ self, month: int = None, year: int = None, **kwargs
264
+ ) -> Tuple[date, date]:
249
265
  dt = self.get_from_date(month=month, year=year, **kwargs)
250
266
  return dt, dt
251
267
 
@@ -289,7 +305,6 @@ class FromToDatesParseMixIn:
289
305
 
290
306
 
291
307
  class SuccessUrlNextMixIn:
292
-
293
308
  def has_next_url(self):
294
309
  return self.request.GET.get('next') is not None
295
310
 
@@ -312,7 +327,9 @@ class DjangoLedgerSecurityMixIn(LoginRequiredMixin, PermissionRequiredMixin):
312
327
 
313
328
  def get_context_data(self, **kwargs):
314
329
  context = super().get_context_data(**kwargs)
315
- context[self.ENTITY_MODEL_CONTEXT_NAME] = self.get_authorized_entity_instance(raise_exception=False)
330
+ context[self.ENTITY_MODEL_CONTEXT_NAME] = self.get_authorized_entity_instance(
331
+ raise_exception=False
332
+ )
316
333
  return context
317
334
 
318
335
  def get_login_url(self):
@@ -323,9 +340,7 @@ class DjangoLedgerSecurityMixIn(LoginRequiredMixin, PermissionRequiredMixin):
323
340
 
324
341
  def get_entity_slug_kwarg(self):
325
342
  if self.ENTITY_SLUG_URL_KWARG is None:
326
- raise ImproperlyConfigured(
327
- _('ENTITY_SLUG_URL_KWARG must be provided.')
328
- )
343
+ raise ImproperlyConfigured(_('ENTITY_SLUG_URL_KWARG must be provided.'))
329
344
  return self.ENTITY_SLUG_URL_KWARG
330
345
 
331
346
  def get_superuser_authorization(self):
@@ -342,7 +357,9 @@ class DjangoLedgerSecurityMixIn(LoginRequiredMixin, PermissionRequiredMixin):
342
357
  if self.request.user.is_authenticated:
343
358
  if entity_slug_kwarg in self.kwargs:
344
359
  try:
345
- self.AUTHORIZED_ENTITY_MODEL = entity_model_qs.get(slug__exact=self.kwargs[entity_slug_kwarg])
360
+ self.AUTHORIZED_ENTITY_MODEL = entity_model_qs.get(
361
+ slug__exact=self.kwargs[entity_slug_kwarg]
362
+ )
346
363
  except ObjectDoesNotExist:
347
364
  return False
348
365
  return True
@@ -354,7 +371,9 @@ class DjangoLedgerSecurityMixIn(LoginRequiredMixin, PermissionRequiredMixin):
354
371
  authorized_superuser=self.get_superuser_authorization(),
355
372
  )
356
373
 
357
- def get_authorized_entity_instance(self, raise_exception: bool = True) -> Optional[EntityModel]:
374
+ def get_authorized_entity_instance(
375
+ self, raise_exception: bool = True
376
+ ) -> Optional[EntityModel]:
358
377
  if self.AUTHORIZED_ENTITY_MODEL is None:
359
378
  if raise_exception:
360
379
  raise Http404()
@@ -383,10 +402,9 @@ class EntityUnitMixIn:
383
402
  unit_slug = self.get_unit_slug()
384
403
  context['unit_slug'] = unit_slug
385
404
 
386
- by_unit = any([
387
- True if unit_slug else False,
388
- self.request.GET.get('by_unit') is not None
389
- ])
405
+ by_unit = any(
406
+ [True if unit_slug else False, self.request.GET.get('by_unit') is not None]
407
+ )
390
408
 
391
409
  context['by_unit'] = by_unit
392
410
  return context
@@ -418,17 +436,8 @@ class DigestContextMixIn:
418
436
  context = super(DigestContextMixIn, self).get_context_data(**kwargs)
419
437
  return self.get_io_digest(context=context, **kwargs)
420
438
 
421
- def get_io_digest(self,
422
- context,
423
- from_date=None,
424
- to_date=None,
425
- **kwargs):
426
-
427
- if any([
428
- self.IO_DIGEST_UNBOUNDED,
429
- self.IO_DIGEST_BOUNDED
430
- ]):
431
-
439
+ def get_io_digest(self, context, from_date=None, to_date=None, **kwargs):
440
+ if any([self.IO_DIGEST_UNBOUNDED, self.IO_DIGEST_BOUNDED]):
432
441
  by_period = self.request.GET.get('by_period')
433
442
  io_model: EntityModel | LedgerModel = self.object
434
443
  if not to_date:
@@ -445,17 +454,21 @@ class DigestContextMixIn:
445
454
  if self.IO_DIGEST_UNBOUNDED:
446
455
  io_digest = io_model.digest(
447
456
  user_model=self.request.user,
448
- entity_slug=io_model.entity_slug if isinstance(io_model, LedgerModel) else None,
457
+ entity_slug=io_model.entity_slug
458
+ if isinstance(io_model, LedgerModel)
459
+ else None,
449
460
  to_date=to_date,
450
461
  unit_slug=unit_slug,
451
462
  by_period=True if by_period else False,
452
463
  process_ratios=True,
453
464
  process_roles=True,
454
- process_groups=True
465
+ process_groups=True,
455
466
  )
456
467
 
457
468
  context[self.get_io_manager_unbounded_context_name()] = io_digest
458
- context[self.get_io_digest_unbounded_context_name()] = io_digest.get_io_data()
469
+ context[self.get_io_digest_unbounded_context_name()] = (
470
+ io_digest.get_io_data()
471
+ )
459
472
 
460
473
  if self.IO_DIGEST_BOUNDED:
461
474
  io_digest_equity = io_model.digest(
@@ -467,11 +480,13 @@ class DigestContextMixIn:
467
480
  by_period=True if by_period else False,
468
481
  process_ratios=True,
469
482
  process_roles=True,
470
- process_groups=True
483
+ process_groups=True,
471
484
  )
472
485
 
473
486
  context[self.get_io_manager_bounded_context_name()] = io_digest_equity
474
- context[self.get_io_digest_bounded_context_name()] = io_digest_equity.get_io_data()
487
+ context[self.get_io_digest_bounded_context_name()] = (
488
+ io_digest_equity.get_io_data()
489
+ )
475
490
 
476
491
  # todo: how is this used??....
477
492
  context['date_filter'] = to_date
@@ -493,18 +508,20 @@ class UnpaidElementsMixIn:
493
508
  from_date = context['from_date'] if not from_date else from_date
494
509
  to_date = context['to_date'] if not to_date else to_date
495
510
 
496
- qs = InvoiceModel.objects.for_entity(
497
- entity_model=self.kwargs['entity_slug']
498
- ).for_user(
499
- user_model=self.request.user
500
- ).approved().filter(
501
- Q(date_approved__gte=from_date) &
502
- Q(date_approved__lte=to_date)
503
- ).select_related('customer').order_by('date_due')
511
+ qs = (
512
+ InvoiceModel.objects.for_entity(entity_model=self.kwargs['entity_slug'])
513
+ .for_user(user_model=self.request.user)
514
+ .approved()
515
+ .filter(Q(date_approved__gte=from_date) & Q(date_approved__lte=to_date))
516
+ .select_related('customer')
517
+ .order_by('date_due')
518
+ )
504
519
 
505
520
  unit_slug = self.get_unit_slug()
506
521
  if unit_slug:
507
- qs = qs.filter(ledger__journal_entries__entity_unit__slug__exact=unit_slug)
522
+ qs = qs.filter(
523
+ ledger__journal_entries__entity_unit__slug__exact=unit_slug
524
+ )
508
525
 
509
526
  return qs
510
527
 
@@ -513,19 +530,21 @@ class UnpaidElementsMixIn:
513
530
  from_date = context['from_date'] if not from_date else from_date
514
531
  to_date = context['to_date'] if not to_date else to_date
515
532
 
516
- qs = BillModel.objects.for_entity(
517
- entity_model=self.kwargs['entity_slug']
518
- ).for_user(
519
- user_model=self.request.user
520
- ).unpaid().filter(
521
- Q(date_approved__gte=from_date) &
522
- Q(date_approved__lte=to_date)
523
- ).select_related('vendor').order_by('date_due')
533
+ qs = (
534
+ BillModel.objects.for_entity(entity_model=self.kwargs['entity_slug'])
535
+ .for_user(user_model=self.request.user)
536
+ .unpaid()
537
+ .filter(Q(date_approved__gte=from_date) & Q(date_approved__lte=to_date))
538
+ .select_related('vendor')
539
+ .order_by('date_due')
540
+ )
524
541
 
525
542
  unit_slug = self.get_unit_slug()
526
543
 
527
544
  if unit_slug:
528
- qs = qs.filter(ledger__journal_entries__entity_unit__slug__exact=unit_slug)
545
+ qs = qs.filter(
546
+ ledger__journal_entries__entity_unit__slug__exact=unit_slug
547
+ )
529
548
 
530
549
  return qs
531
550
 
@@ -536,7 +555,7 @@ class BaseDateNavigationUrlMixIn:
536
555
  'unit_slug',
537
556
  'ledger_pk',
538
557
  'account_pk',
539
- 'coa_slug'
558
+ 'coa_slug',
540
559
  )
541
560
 
542
561
  def get_context_data(self, **kwargs):
@@ -550,9 +569,9 @@ class BaseDateNavigationUrlMixIn:
550
569
  context['date_navigation_url'] = reverse(
551
570
  f'django_ledger:{view_name_base}',
552
571
  kwargs={
553
- k: v for k, v in self.kwargs.items() if
554
- k in self.BASE_DATE_URL_KWARGS
555
- })
572
+ k: v for k, v in self.kwargs.items() if k in self.BASE_DATE_URL_KWARGS
573
+ },
574
+ )
556
575
 
557
576
 
558
577
  class PDFReportMixIn:
@@ -574,7 +593,9 @@ class PDFReportMixIn:
574
593
 
575
594
  def get_pdf_func_name(self):
576
595
  if not self.pdf_report_type:
577
- raise NotImplementedError(f'Must define pdf_report_type from {self.PDFReportEnum.__name__}')
596
+ raise NotImplementedError(
597
+ f'Must define pdf_report_type from {self.PDFReportEnum.__name__}'
598
+ )
578
599
  return self.pdf_io_mixin_function_map[self.pdf_report_type]
579
600
 
580
601
  def get_pdf(self):
@@ -586,7 +607,7 @@ class PDFReportMixIn:
586
607
  from_date=self.get_pdf_from_date(),
587
608
  to_date=self.get_pdf_to_date(),
588
609
  user_model=self.request.user,
589
- subtitle=self.get_pdf_subtitle()
610
+ subtitle=self.get_pdf_subtitle(),
590
611
  )
591
612
  pdf.create_pdf_report()
592
613
  return pdf
@@ -606,12 +627,17 @@ class PDFReportMixIn:
606
627
  pdf = self.get_pdf()
607
628
  response = HttpResponse(
608
629
  bytes(pdf.output()),
609
- content_type="application/pdf",
630
+ content_type='application/pdf',
631
+ )
632
+ response.headers['Content-Disposition'] = (
633
+ f'attachment; filename={pdf.get_pdf_filename()}'
610
634
  )
611
- response.headers['Content-Disposition'] = f'attachment; filename={pdf.get_pdf_filename()}'
612
635
  return response
613
636
 
614
637
  def get(self, request, **kwargs):
615
- if request.GET.get(self.pdf_format_query_param) == self.pdf_format_query_param_value:
638
+ if (
639
+ request.GET.get(self.pdf_format_query_param)
640
+ == self.pdf_format_query_param_value
641
+ ):
616
642
  return self.get_pdf_response()
617
643
  return super().get(request, **kwargs)