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
@@ -35,6 +35,7 @@ urlpatterns = [
35
35
  path('feedback/', include('django_ledger.urls.feedback')),
36
36
  path('inventory/', include('django_ledger.urls.inventory')),
37
37
  path('home/', include('django_ledger.urls.home')),
38
+ path('receipt/', include('django_ledger.urls.receipt')),
38
39
  path('api/', include('django_ledger.urls.djl_api')),
39
40
  path('', views.RootUrlView.as_view(), name='root'),
40
41
  ]
@@ -7,4 +7,7 @@ urlpatterns = [
7
7
  path('<slug:entity_slug>/update/<uuid:customer_pk>/',
8
8
  views.CustomerModelUpdateView.as_view(),
9
9
  name='customer-update'),
10
+ path('<slug:entity_slug>/detail/<uuid:customer_pk>/',
11
+ views.CustomerModelDetailView.as_view(),
12
+ name='customer-detail'),
10
13
  ]
@@ -18,4 +18,7 @@ urlpatterns = [
18
18
  path('<slug:entity_slug>/jobs/<uuid:job_pk>/txs/',
19
19
  views.DataImportJobDetailView.as_view(),
20
20
  name='data-import-job-txs'),
21
+ path('<slug:entity_slug>/jobs/<uuid:job_pk>/txs/<uuid:staged_tx_pk>/undo/',
22
+ views.StagedTransactionUndoView.as_view(),
23
+ name='data-import-staged-tx-undo'),
21
24
  ]
@@ -0,0 +1,102 @@
1
+ """
2
+ Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
+ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
+
5
+ Contributions to this module:
6
+ * Miguel Sanda <msanda@arrobalytics.com>
7
+ """
8
+
9
+ from django.urls import path
10
+
11
+ from django_ledger import views
12
+
13
+ urlpatterns = [
14
+ path(
15
+ '<slug:entity_slug>/latest/',
16
+ views.ReceiptModelListView.as_view(),
17
+ name='receipt-list',
18
+ ),
19
+ path(
20
+ '<slug:entity_slug>/year/<int:year>/',
21
+ views.ReceiptModelYearListView.as_view(),
22
+ name='receipt-list-year',
23
+ ),
24
+ path(
25
+ '<slug:entity_slug>/quarter/<int:year>/q<int:quarter>/',
26
+ views.ReceiptModelQuarterListView.as_view(),
27
+ name='receipt-list-quarter',
28
+ ),
29
+ path(
30
+ '<slug:entity_slug>/month/<int:year>/<int:month>/',
31
+ views.ReceiptModelMonthListView.as_view(),
32
+ name='receipt-list-month',
33
+ ),
34
+ path(
35
+ '<slug:entity_slug>/detail/<uuid:receipt_pk>/',
36
+ views.ReceiptModelDetailView.as_view(),
37
+ name='receipt-detail',
38
+ ),
39
+ path(
40
+ '<slug:entity_slug>/delete/<uuid:receipt_pk>/',
41
+ views.ReceiptModelDeleteView.as_view(),
42
+ name='receipt-delete',
43
+ ),
44
+ # Filtered lists
45
+ path(
46
+ '<slug:entity_slug>/type/<str:receipt_type>/',
47
+ views.ReceiptModelListView.as_view(),
48
+ name='receipt-list-type',
49
+ ),
50
+ path(
51
+ '<slug:entity_slug>/vendor/<uuid:vendor_pk>/',
52
+ views.ReceiptModelListView.as_view(),
53
+ name='receipt-list-vendor',
54
+ ),
55
+ path(
56
+ '<slug:entity_slug>/customer/<uuid:customer_pk>/',
57
+ views.ReceiptModelListView.as_view(),
58
+ name='receipt-list-customer',
59
+ ),
60
+ # Reports: Vendor
61
+ path(
62
+ '<slug:entity_slug>/report/vendor/<uuid:vendor_pk>/latest/',
63
+ views.VendorReceiptReportListView.as_view(),
64
+ name='receipt-report-vendor',
65
+ ),
66
+ path(
67
+ '<slug:entity_slug>/report/vendor/<uuid:vendor_pk>/year/<int:year>/',
68
+ views.VendorReceiptReportYearListView.as_view(),
69
+ name='receipt-report-vendor-year',
70
+ ),
71
+ path(
72
+ '<slug:entity_slug>/report/vendor/<uuid:vendor_pk>/quarter/<int:year>/q<int:quarter>/',
73
+ views.VendorReceiptReportQuarterListView.as_view(),
74
+ name='receipt-report-vendor-quarter',
75
+ ),
76
+ path(
77
+ '<slug:entity_slug>/report/vendor/<uuid:vendor_pk>/month/<int:year>/<int:month>/',
78
+ views.VendorReceiptReportMonthListView.as_view(),
79
+ name='receipt-report-vendor-month',
80
+ ),
81
+ # Reports: Customer
82
+ path(
83
+ '<slug:entity_slug>/report/customer/<uuid:customer_pk>/latest/',
84
+ views.CustomerReceiptReportListView.as_view(),
85
+ name='receipt-report-customer',
86
+ ),
87
+ path(
88
+ '<slug:entity_slug>/report/customer/<uuid:customer_pk>/year/<int:year>/',
89
+ views.CustomerReceiptReportYearListView.as_view(),
90
+ name='receipt-report-customer-year',
91
+ ),
92
+ path(
93
+ '<slug:entity_slug>/report/customer/<uuid:customer_pk>/quarter/<int:year>/q<int:quarter>/',
94
+ views.CustomerReceiptReportQuarterListView.as_view(),
95
+ name='receipt-report-customer-quarter',
96
+ ),
97
+ path(
98
+ '<slug:entity_slug>/report/customer/<uuid:customer_pk>/month/<int:year>/<int:month>/',
99
+ views.CustomerReceiptReportMonthListView.as_view(),
100
+ name='receipt-report-customer-month',
101
+ ),
102
+ ]
@@ -5,4 +5,5 @@ urlpatterns = [
5
5
  path('<slug:entity_slug>/list/', views.VendorModelListView.as_view(), name='vendor-list'),
6
6
  path('<slug:entity_slug>/create/', views.VendorModelCreateView.as_view(), name='vendor-create'),
7
7
  path('<slug:entity_slug>/update/<uuid:vendor_pk>/', views.VendorModelUpdateView.as_view(), name='vendor-update'),
8
+ path('<slug:entity_slug>/detail/<uuid:vendor_pk>/', views.VendorModelDetailView.as_view(), name='vendor-detail'),
8
9
  ]
@@ -28,3 +28,4 @@ from django_ledger.views.transactions import *
28
28
  from django_ledger.views.unit import *
29
29
  from django_ledger.views.vendor import *
30
30
  from django_ledger.views.closing_entry import *
31
+ from django_ledger.views.receipt import *
@@ -5,15 +5,24 @@ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
5
5
  Contributions to this module:
6
6
  * Miguel Sanda <msanda@arrobalytics.com>
7
7
  """
8
+
8
9
  from typing import Optional
9
10
 
10
11
  from django.urls import reverse
11
12
  from django.utils.translation import gettext_lazy as _
12
- from django.views.generic import ListView, CreateView, UpdateView, DeleteView
13
+ from django.views.generic import (
14
+ CreateView,
15
+ DeleteView,
16
+ DetailView,
17
+ ListView,
18
+ UpdateView,
19
+ )
13
20
 
14
21
  from django_ledger.forms.customer import CustomerModelForm
15
22
  from django_ledger.models.customer import CustomerModel, CustomerModelQueryset
16
23
  from django_ledger.models.entity import EntityModel
24
+ from django_ledger.models.invoice import InvoiceModel
25
+ from django_ledger.models.receipt import ReceiptModel
17
26
  from django_ledger.views.mixins import DjangoLedgerSecurityMixIn
18
27
 
19
28
 
@@ -34,7 +43,7 @@ class CustomerModelListView(CustomerModelModelViewQuerySetMixIn, ListView):
34
43
  extra_context = {
35
44
  'page_title': PAGE_TITLE,
36
45
  'header_title': PAGE_TITLE,
37
- 'header_subtitle_icon': 'dashicons:businesswoman'
46
+ 'header_subtitle_icon': 'dashicons:businesswoman',
38
47
  }
39
48
  context_object_name = 'customers'
40
49
 
@@ -47,20 +56,20 @@ class CustomerModelCreateView(CustomerModelModelViewQuerySetMixIn, CreateView):
47
56
  extra_context = {
48
57
  'page_title': PAGE_TITLE,
49
58
  'header_title': PAGE_TITLE,
50
- 'header_subtitle_icon': 'dashicons:businesswoman'
59
+ 'header_subtitle_icon': 'dashicons:businesswoman',
51
60
  }
52
61
 
53
62
  def get_success_url(self):
54
- return reverse('django_ledger:customer-list',
55
- kwargs={
56
- 'entity_slug': self.kwargs['entity_slug']
57
- })
63
+ return reverse(
64
+ 'django_ledger:customer-list',
65
+ kwargs={'entity_slug': self.kwargs['entity_slug']},
66
+ )
58
67
 
59
68
  def form_valid(self, form):
60
69
  customer_model: CustomerModel = form.save(commit=False)
61
- entity_model = EntityModel.objects.for_user(
62
- user_model=self.request.user
63
- ).get(slug__exact=self.kwargs['entity_slug'])
70
+ entity_model = EntityModel.objects.for_user(user_model=self.request.user).get(
71
+ slug__exact=self.kwargs['entity_slug']
72
+ )
64
73
  customer_model.entity_model = entity_model
65
74
  customer_model.save()
66
75
  return super().form_valid(form)
@@ -84,10 +93,10 @@ class CustomerModelUpdateView(CustomerModelModelViewQuerySetMixIn, UpdateView):
84
93
  return context
85
94
 
86
95
  def get_success_url(self):
87
- return reverse('django_ledger:customer-list',
88
- kwargs={
89
- 'entity_slug': self.kwargs['entity_slug']
90
- })
96
+ return reverse(
97
+ 'django_ledger:customer-list',
98
+ kwargs={'entity_slug': self.kwargs['entity_slug']},
99
+ )
91
100
 
92
101
  def form_valid(self, form):
93
102
  form.save()
@@ -96,3 +105,36 @@ class CustomerModelUpdateView(CustomerModelModelViewQuerySetMixIn, UpdateView):
96
105
 
97
106
  class CustomerModelDeleteView(CustomerModelModelViewQuerySetMixIn, DeleteView):
98
107
  pass
108
+
109
+
110
+ class CustomerModelDetailView(CustomerModelModelViewQuerySetMixIn, DetailView):
111
+ template_name = 'django_ledger/customer/customer_detail.html'
112
+ context_object_name = 'customer'
113
+ PAGE_TITLE = _('Customer Details')
114
+ slug_url_kwarg = 'customer_pk'
115
+ slug_field = 'uuid'
116
+
117
+ def get_context_data(self, **kwargs):
118
+ context = super().get_context_data(**kwargs)
119
+ customer: CustomerModel = self.object
120
+ receipts_qs = (
121
+ ReceiptModel.objects.for_entity(entity_model=self.AUTHORIZED_ENTITY_MODEL)
122
+ .for_customer(customer_model=customer)
123
+ .order_by('-updated')
124
+ )
125
+ invoices_qs = (
126
+ InvoiceModel.objects.for_entity(entity_model=self.AUTHORIZED_ENTITY_MODEL)
127
+ .filter(customer=customer)
128
+ .order_by('-updated')
129
+ )
130
+ context.update(
131
+ {
132
+ 'page_title': self.PAGE_TITLE,
133
+ 'header_title': self.PAGE_TITLE,
134
+ 'header_subtitle': f'{customer.customer_name} · {customer.customer_number}',
135
+ 'header_subtitle_icon': 'dashicons:businesswoman',
136
+ 'receipts': receipts_qs,
137
+ 'invoices': invoices_qs,
138
+ }
139
+ )
140
+ return context
@@ -5,17 +5,25 @@ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
5
5
  Contributions to this module:
6
6
  * Miguel Sanda <msanda@arrobalytics.com>
7
7
  """
8
+
8
9
  from datetime import datetime, time
10
+ from typing import Optional
9
11
 
10
12
  from django.contrib import messages
13
+ from django.shortcuts import get_object_or_404, redirect
11
14
  from django.urls import reverse
12
15
  from django.utils.timezone import make_aware
13
16
  from django.utils.translation import gettext_lazy as _
14
- from django.views.generic import ListView, FormView, DetailView, UpdateView, DeleteView
15
-
16
- from django_ledger.forms.data_import import ImportJobModelCreateForm, ImportJobModelUpdateForm
17
- from django_ledger.forms.data_import import StagedTransactionModelFormSet
17
+ from django.views import View
18
+ from django.views.generic import DeleteView, DetailView, FormView, ListView, UpdateView
19
+
20
+ from django_ledger.forms.data_import import (
21
+ ImportJobModelCreateForm,
22
+ ImportJobModelUpdateForm,
23
+ StagedTransactionModelFormSet,
24
+ )
18
25
  from django_ledger.io.ofx import OFXFileManager
26
+ from django_ledger.models import StagedTransactionModelValidationError
19
27
  from django_ledger.models.data_import import ImportJobModel, StagedTransactionModel
20
28
  from django_ledger.views.mixins import DjangoLedgerSecurityMixIn
21
29
 
@@ -25,13 +33,17 @@ class ImportJobModelViewBaseView(DjangoLedgerSecurityMixIn):
25
33
 
26
34
  def get_queryset(self):
27
35
  if self.queryset is None:
28
- self.queryset = ImportJobModel.objects.for_entity(
29
- entity_model=self.AUTHORIZED_ENTITY_MODEL,
30
- ).order_by('-created').select_related(
31
- 'bank_account_model',
32
- 'bank_account_model__entity_model',
33
- 'bank_account_model__account_model',
34
- 'bank_account_model__account_model__coa_model'
36
+ self.queryset = (
37
+ ImportJobModel.objects.for_entity(
38
+ entity_model=self.AUTHORIZED_ENTITY_MODEL,
39
+ )
40
+ .order_by('-created')
41
+ .select_related(
42
+ 'bank_account_model',
43
+ 'bank_account_model__entity_model',
44
+ 'bank_account_model__account_model',
45
+ 'bank_account_model__account_model__coa_model',
46
+ )
35
47
  )
36
48
  return self.queryset
37
49
 
@@ -39,23 +51,19 @@ class ImportJobModelViewBaseView(DjangoLedgerSecurityMixIn):
39
51
  class ImportJobModelCreateView(ImportJobModelViewBaseView, FormView):
40
52
  template_name = 'django_ledger/data_import/import_job_create.html'
41
53
  PAGE_TITLE = _('Create Import Job')
42
- extra_context = {
43
- 'page_title': PAGE_TITLE,
44
- 'header_title': PAGE_TITLE
45
- }
54
+ extra_context = {'page_title': PAGE_TITLE, 'header_title': PAGE_TITLE}
46
55
  form_class = ImportJobModelCreateForm
47
56
 
48
57
  def get_form(self, form_class=None, **kwargs):
49
58
  return self.form_class(
50
- entity_model=self.get_authorized_entity_instance(),
51
- **self.get_form_kwargs()
59
+ entity_model=self.get_authorized_entity_instance(), **self.get_form_kwargs()
52
60
  )
53
61
 
54
62
  def get_success_url(self):
55
- return reverse('django_ledger:data-import-jobs-list',
56
- kwargs={
57
- 'entity_slug': self.kwargs['entity_slug']
58
- })
63
+ return reverse(
64
+ 'django_ledger:data-import-jobs-list',
65
+ kwargs={'entity_slug': self.kwargs['entity_slug']},
66
+ )
59
67
 
60
68
  def form_valid(self, form):
61
69
  ofx_manager = OFXFileManager(ofx_file_or_path=form.files['ofx_file'])
@@ -66,13 +74,16 @@ class ImportJobModelCreateView(ImportJobModelViewBaseView, FormView):
66
74
  txs_to_stage = ofx_manager.get_account_txs()
67
75
  staged_txs_model_list = [
68
76
  StagedTransactionModel(
69
- date_posted=make_aware(value=datetime.combine(date=tx.dtposted.date(), time=time.min)),
77
+ date_posted=make_aware(
78
+ value=datetime.combine(date=tx.dtposted.date(), time=time.min)
79
+ ),
70
80
  fit_id=tx.fitid,
71
81
  amount=tx.trnamt,
72
82
  import_job=import_job,
73
83
  name=tx.name,
74
- memo=tx.memo
75
- ) for tx in txs_to_stage
84
+ memo=tx.memo,
85
+ )
86
+ for tx in txs_to_stage
76
87
  ]
77
88
  for tx in staged_txs_model_list:
78
89
  tx.clean()
@@ -83,10 +94,7 @@ class ImportJobModelCreateView(ImportJobModelViewBaseView, FormView):
83
94
 
84
95
  class ImportJobModelListView(ImportJobModelViewBaseView, ListView):
85
96
  PAGE_TITLE = _('Data Import Jobs')
86
- extra_context = {
87
- 'page_title': PAGE_TITLE,
88
- 'header_title': PAGE_TITLE
89
- }
97
+ extra_context = {'page_title': PAGE_TITLE, 'header_title': PAGE_TITLE}
90
98
  context_object_name = 'import_jobs'
91
99
  template_name = 'django_ledger/data_import/data_import_job_list.html'
92
100
 
@@ -99,9 +107,10 @@ class ImportJobModelUpdateView(ImportJobModelViewBaseView, UpdateView):
99
107
 
100
108
  def get_context_data(self, **kwargs):
101
109
  ctx = super().get_context_data(**kwargs)
110
+ import_job_model: ImportJobModel = self.object
102
111
  ctx['page_title'] = 'Import Job Update'
103
112
  ctx['header_title'] = 'Import Job Update'
104
- ctx['header_subtitle'] = self.object.description
113
+ ctx['header_subtitle'] = import_job_model.description
105
114
  ctx['header_subtitle_icon'] = 'solar:import-bold'
106
115
  return ctx
107
116
 
@@ -109,9 +118,7 @@ class ImportJobModelUpdateView(ImportJobModelViewBaseView, UpdateView):
109
118
  entity_model = self.get_authorized_entity_instance()
110
119
  return reverse(
111
120
  viewname='django_ledger:data-import-jobs-list',
112
- kwargs={
113
- 'entity_slug': entity_model.slug
114
- }
121
+ kwargs={'entity_slug': entity_model.slug},
115
122
  )
116
123
 
117
124
  def form_valid(self, form):
@@ -119,7 +126,7 @@ class ImportJobModelUpdateView(ImportJobModelViewBaseView, UpdateView):
119
126
  self.request,
120
127
  level=messages.SUCCESS,
121
128
  message=_(f'Successfully updated Import Job {self.object.description}'),
122
- extra_tags='is-success'
129
+ extra_tags='is-success',
123
130
  )
124
131
  return super().form_valid(form=form)
125
132
 
@@ -140,9 +147,7 @@ class ImportJobModelDeleteView(ImportJobModelViewBaseView, DeleteView):
140
147
  def get_success_url(self):
141
148
  return reverse(
142
149
  viewname='django_ledger:data-import-jobs-list',
143
- kwargs={
144
- 'entity_slug': self.AUTHORIZED_ENTITY_MODEL.slug
145
- }
150
+ kwargs={'entity_slug': self.AUTHORIZED_ENTITY_MODEL.slug},
146
151
  )
147
152
 
148
153
 
@@ -161,10 +166,10 @@ class DataImportJobDetailView(ImportJobModelViewBaseView, DetailView):
161
166
  'import_job_model': self.get_object(),
162
167
  }
163
168
 
164
- def get_context_data(self, **kwargs):
169
+ def get_context_data(
170
+ self, txs_formset: Optional[StagedTransactionModelFormSet] = None, **kwargs
171
+ ):
165
172
  context = super().get_context_data(**kwargs)
166
-
167
- # job_model: ImportJobModel = getattr(self, 'object', self.get_object())
168
173
  job_model: ImportJobModel = self.object
169
174
 
170
175
  context['page_title'] = job_model.description
@@ -172,9 +177,13 @@ class DataImportJobDetailView(ImportJobModelViewBaseView, DetailView):
172
177
  context['header_subtitle'] = job_model.description
173
178
  context['header_subtitle_icon'] = 'tabler:table-import'
174
179
 
175
- staged_txs_formset = StagedTransactionModelFormSet(
176
- entity_model=self.get_authorized_entity_instance(),
177
- import_job_model=job_model
180
+ staged_txs_formset = (
181
+ StagedTransactionModelFormSet(
182
+ entity_model=self.get_authorized_entity_instance(),
183
+ import_job_model=job_model,
184
+ )
185
+ if not txs_formset
186
+ else txs_formset
178
187
  )
179
188
 
180
189
  context['staged_txs_formset'] = staged_txs_formset
@@ -183,35 +192,79 @@ class DataImportJobDetailView(ImportJobModelViewBaseView, DetailView):
183
192
 
184
193
  def post(self, request, **kwargs):
185
194
  self.object = self.get_object()
195
+ import_job_model: ImportJobModel = self.object
186
196
 
187
197
  txs_formset = StagedTransactionModelFormSet(
188
198
  entity_model=self.get_authorized_entity_instance(),
189
- import_job_model=self.object,
190
- data=request.POST
199
+ import_job_model=import_job_model,
200
+ data=request.POST,
191
201
  )
192
202
 
193
- # if txs_formset.is_valid():
194
- for tx_form in txs_formset:
195
- if tx_form.has_changed():
196
- # perform work only if form has changed...
197
- if tx_form.is_valid():
198
- tx_form.save()
199
- # import entry was selected to be split....
200
- is_split = tx_form.cleaned_data['tx_split'] is True
201
- if is_split:
202
- tx_form.instance.add_split()
203
-
204
- # import entry was selected for import...
205
- is_import = tx_form.cleaned_data['tx_import']
206
- if is_import:
207
- # all entries in split will be going so the same journal entry... (same unit...)
208
- is_bundled = tx_form.cleaned_data['bundle_split']
209
- tx_form.instance.migrate() if is_bundled else tx_form.instance.migrate(split_txs=True)
210
-
211
- messages.add_message(request,
212
- messages.SUCCESS,
213
- 'Successfully saved transactions.',
214
- extra_tags='is-success')
203
+ if txs_formset.has_changed():
204
+ if txs_formset.is_valid():
205
+ txs_formset.save()
206
+ for tx_form in txs_formset:
207
+ if tx_form.has_changed():
208
+ staged_transaction_model: StagedTransactionModel = (
209
+ tx_form.instance
210
+ )
211
+ is_split = tx_form.cleaned_data['tx_split'] is True
212
+ if is_split:
213
+ staged_transaction_model.add_split()
214
+ # import entry was selected for import...
215
+ is_import = tx_form.cleaned_data['tx_import']
216
+ if is_import:
217
+ # all entries in split will be going so the same journal entry... (same unit...)
218
+ is_bundled = tx_form.cleaned_data['bundle_split']
219
+ if staged_transaction_model.can_migrate_receipt():
220
+ staged_transaction_model.migrate_receipt(
221
+ receipt_date=staged_transaction_model.date_posted
222
+ )
223
+ else:
224
+ staged_transaction_model.migrate_transactions(
225
+ split_txs=not is_bundled
226
+ )
227
+ else:
228
+ context = self.get_context_data(txs_formset=txs_formset, **kwargs)
229
+ return self.render_to_response(context)
230
+
231
+ messages.add_message(
232
+ request,
233
+ messages.SUCCESS,
234
+ 'Successfully saved transactions.',
235
+ extra_tags='is-success',
236
+ )
215
237
 
216
238
  context = self.get_context_data(**kwargs)
217
239
  return self.render_to_response(context)
240
+
241
+
242
+ class StagedTransactionUndoView(ImportJobModelViewBaseView, View):
243
+ http_method_names = ['post']
244
+
245
+ def post(self, request, entity_slug, job_pk, staged_tx_pk, *args, **kwargs):
246
+ import_job_model = get_object_or_404(self.get_queryset(), uuid__exact=job_pk)
247
+ staged_txs_qs = import_job_model.stagedtransactionmodel_set.all()
248
+ staged_tx = get_object_or_404(staged_txs_qs, uuid__exact=staged_tx_pk)
249
+ try:
250
+ staged_tx.undo_import()
251
+ messages.add_message(
252
+ request,
253
+ messages.SUCCESS,
254
+ _('Successfully undone import for transaction %(name)s.')
255
+ % {'name': staged_tx.name or staged_tx.fit_id},
256
+ extra_tags='is-success',
257
+ )
258
+ except StagedTransactionModelValidationError as e:
259
+ messages.add_message(
260
+ request,
261
+ messages.ERROR,
262
+ e.message,
263
+ extra_tags='is-danger',
264
+ )
265
+ return redirect(
266
+ reverse(
267
+ 'django_ledger:data-import-job-txs',
268
+ kwargs={'entity_slug': entity_slug, 'job_pk': job_pk},
269
+ )
270
+ )